mcpField
V
vid-motion-graphics

vid-motion-graphics

github.com/varnan-tech创意与媒体68745 次安装2 周前更新
代码仓库

关于 vid-motion-graphics

vid-motion-graphics 是一项将多场景 HTML/CSS 动画渲染为 MP4 的创作技能,适用于需要制作图表或信息视频的创作者。它通过逐帧控制动画并用 FFmpeg 合成画面,强调确定性渲染。

主要功能

  1. 01生成多场景 MP4 视频
  2. 02支持 1–6 个场景
  3. 03提供五种视觉风格
  4. 04可设置画幅、帧率和配乐
  5. 05通过 Chromium 与 FFmpeg 导出

适用场景

  1. 01将内容简报制作成动态图形视频
  2. 02制作包含数据和要点的短片
  3. 03按平台画幅导出视频

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/varnan-tech/opendirectory --skill vid-motion-graphics

安装后在 Claude Code 中输入/vid-motion-graphics确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
evals/
evals.json
references/
scene-library.md
style-presets.md
scripts/
capture-frames.mjs
export-video.sh

详细介绍

vid-motion-graphics

将多场景动态图形生成为 MP4。通过 Playwright 在无头 Chromium 中渲染 HTML/CSS 动画(使用 Web Animations API 跳转帧),再用 FFmpeg 组装 PNG 帧。无需 React、AI API 或 Python——除 graphic-gif 系列之外,不新增任何依赖。

仅使用 CDN 字体。HTML 中不使用外部库。


关键规则(每次生成前阅读)

  1. 使用 window.renderFrame(t)——场景转场不要使用 CSS @keyframes。 在 Chromium 中,CSS 动画的 currentTime 在向后跳转时会被静默忽略。renderFrame 方法:纯 JS 函数直接根据毫秒数计算 opacity/transform。Playwright 每帧调用一次。确定性高,无竞态。
  2. 任何元素都不要设置 animation-delay。 使用 renderFrame 时不需要。如果发现自己要写 animation-delay,请停下——你使用的架构不对。
  3. 仅在 document.fonts.ready.then(...) 中设置 window.__videoReady = true。 绝不要同步设置——必须先加载字体,再由 Playwright 捕获第 1 帧,否则文本会使用回退字体渲染。
  4. 暴露 window.__stopPreview()。 浏览器的 rAF 预览循环会与 Playwright 的 evaluate/screenshot 调用发生竞态。capture-frames.mjs 会在帧循环开始前调用 __stopPreview()。务必包含它。
  5. 场景边界检查使用 t < startMs(而不是 <=)。 在第 0 帧,t <= 0 会导致场景 1 显示为黑屏。正确的判断条件是 if (t < startMs || t >= endMs) return hidden。
  6. Body = 精确像素尺寸。 宽度和高度均为整数(1080px、1920px)。不要使用 %、vw/vh 或响应式单位。
  7. 不能同时显示两个场景(10% 的进入重叠除外)。所有场景在各自 renderFrame 时间窗口之外都应为 opacity: 0。
  8. 转场仅使用 opacity。 不要切换 display,也不要使用 visibility——GPU 合成的 opacity 能确保帧级精确。
  9. 绝不要在聊天中贴出 HTML。 保存到文件,只展示摘要。
  10. 标题应表达核心信息,而不是主题。 用“Q4 业绩超额达成目标的 3 个原因”,而不是“2024 年 Q4 业绩视频”。
  11. 生成任何 HTML 前,先阅读 references/scene-library.md。 使用该文件中的确切 HTML 结构和 CSS 类名。

第 1 步:需求收集

必填: content_brief

可选参数及默认值:

参数默认值说明
content_brief—描述视频要传达什么的文本(必填)
scenesauto场景数量(1–6)。auto = 根据简报确定。
duration_per_scene3s每个场景的时长,单位为秒(1–8s)
stylekinetic-darkkinetic-dark / editorial-light / data-pulse / bold-type / minimal-clean
aspect_ratio1:11:1(1080×1080)/ 16:9(1920×1080)/ 9:16(1080×1920)
fps30每秒帧数(24、30 或 60)
musicnone背景音轨的音频文件路径(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)%。

场景起始 %结束 %
10%(100/N)%
2(100/N)%(200/N)%
………
N((N-1)×100/N)%100%

在每个场景的范围内:

  • 进入:场景范围的前 10%
  • 保持:场景范围的 10% 到 85%
  • 退出:场景范围的 85% 到 100%

5. 确定像素尺寸:

  • 1:1 → W=1080, H=1080
  • 16:9 → W=1920, H=1080
  • 9: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。”

❌ 不好:“制作一段关于我们公司的视频”

评论

支持 Markdown