Communitygithub.com

MooT020/kinetic-typography

动感字体的视频,使文字本身成为视觉主体——词句会猛击、拉伸、堆叠、分裂和切换,与画外音或音乐节拍同步,渲染为9:16或16:9的MP4格式。

kinetic-typography 是什么?

该AI Agent技能是一个专门用于创建动态文字视频的工具,专注于让文字本身成为视觉焦点。它支持多种动效,如猛击、拉伸、堆叠、分裂和翻转切换,并能与画外音或音乐节拍同步。开发者可通过提供的一句话核心信息、脚本短语、能量峰值位置和结尾锁定帧来引导视频创作。技能支持9:16竖屏和16:9横屏格式,并提供丰富的可调参数,包括节奏源(音频节拍或固定节拍)、色调(冲击、平静、混乱)以及字体、颜色等品牌风格设置。它基于HyperFrames和GSAP构建,只需Node.js和FFmpeg即可运行,并提供了完整的HTML模板和详细的运动规则,确保视频效果专业且符合设计要求。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/MooT020/takhayyal-video-skills/tree/HEAD/plugins/motion-design-styles/skills/kinetic-typography

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

动感字体 (Kinetic Typography)

制作视频,让文字本身成为画面。一个短语落地、充满画框、被猛烈冲击,然后被下一个短语替代,伴随节奏。它应该给人大胆、快速且富有音乐感的感觉,就像一个移动的海报。

选项

选项默认值说明
format竖屏 1080×1920或横屏 1920×1080,正方形 1080×1080
duration10–20 秒匹配画外音或歌曲段落
tonepunchycalm(0.4秒的滑动和淡入淡出,较少翻转),punchy(默认),chaotic(全大写、猛击、抖动,每隔一个短语翻转一次)
rhythm固定节奏或画外音单词时间,或音乐节拍

简报(规划前回答)

  1. 观众应该记住的一句话是什么?
  2. 将剧本拆分为1–4个单词的短语;每个短语标记一个主词。
  3. 能量峰值在哪里(获得颜色翻转的那个短语)?
  4. 结尾锁定帧是什么(标题、行动号召、用户名)?

可读性表(与节拍列表一同编写)

对于每个节拍,写出观众必须理解的一件事(“机器人不会飞”、“缓存未命中”)以及它保持可读的时间。一次只读一件事。文字停留时间:标签至少停留0.8秒,一个句子完全显示后每个单词约0.3秒。如果文字在时间内放不下,则裁词,而不是加快速度。

运动规则

  • 运动是因果的:屏幕上的每个动作都有原因(点击、落下、数据包到达),并且下一个动作随之而来。
  • 为整个视频选择一个主要运动方向并保持一致。使用其他方向来表示含义:向上表示成功或结论,拉远表示“这是整体画面”。
  • 场景的退出为下一个场景的进入做准备:相同的轴、相同的方向、相似的速度。在变化之间携带一个对象,而不是交叉淡入淡出。
  • 图表、图表或文字上不要有无关的填充动画(浮动、呼吸、摆动)。如果什么都不该动,就保持静止;一次有意的镜头漂移就足够了。
  • 缓动:平滑的ease-out用于进入(power2.out 到 power4.out),较快的ease-in用于退出(约为进入时间的75%)。进入时间最多0.8秒。整个组的交错最多0.5秒。过冲(back.out)仅用于小的弹出效果;不要使用弹性或弹跳。
  • 重要时刻一次只移动一个东西;眼睛只能跟踪一个。
  • 在回报前添加0.3–0.75秒的静止节拍。

风格规则(始终遵守)

这些定义了此风格;无论颜色或文字如何都要保持。

  1. 文字占画框宽度的50–80%,因为小字看起来像字幕,而不是视觉主体。
  2. 一次只显示一个想法:1–4个单词,最多3行。其他所有内容都已离开。
  3. 每个单词都通过一个动作到达(猛击、滑动、拉伸、蒙版揭示、翻转)。没有东西只是淡入。
  4. 变化落在节奏上:每0.25–0.7秒出现一个新单词或动作,关键词在节拍或重读音节上。
  5. 对比度极端:接近黑色的文字配接近白色的背景,或反之,再加上仅用于关键词的一种强调色。
  6. 每个短语有一个主词:最大、强调色或运动方式与其余不同。

默认值(可自由更改)

字体、颜色、文字、时间和背景。如果用户提供了品牌字体或颜色,则以他们的为准。优先级:用户的品牌或规格说明 > 明确要求 > 这些默认值。

  • 背景 #111111,文字 #f4f1ea,强调色 #ff4d2e。每短语可在深色或浅色背景上切换以增强能量。
  • 显示字体:厚重的窄体(Anton、Bebas Neue 或 Archivo Black),主词全大写。辅助字体:厚重的无衬线字体(Inter 800)用于小的连接词。
  • 字号:9:16中主词180–320像素;小词60–90像素。

设置(一次性)

node -v                      # 需要22+
ffmpeg -version
npx hyperframes init my-video --non-interactive --resolution portrait && cd my-video   # landscape for 16:9

在已有的空文件夹内使用 npx hyperframes init . --non-interactive --resolution portrait,然后用下面的模板替换生成的 index.html。

构建前决定

在编写代码之前,将这些反馈给用户,一行即可。

  1. 宽高比: 9:16 (1080×1920) 或 16:9 (1920×1080)。默认是9:16。
  2. 文字和时间: 将剧本拆分为1–4个单词的短语。如果有画外音,从其单词时间戳获取时间(npx hyperframes transcribe voice.wav)。如果没有,给每个短语0.6–1.4秒,给主词更长时间。
  3. 节奏来源: 语音(落在重读音节上)、音乐(落在节拍上;npx hyperframes beats music.mp3)或固定节奏(例如120 BPM = 每0.5秒一个节拍)。
  4. 每个短语的主词 以及它使用的动作。
需求HyperFrames + GSAP (默认)Remotion
单词和字母动作、蒙版、拉伸是是
语音或节拍数据的时机是 (labels)是 (frames)
仅需Node和FFmpeg即可运行是是
许可证免费最多3人免费,以上收费

动作词汇表

动作方式 (GSAP)用于
Slamfrom {scale: 2.6, opacity: 0},expo.out,0.22秒,外加6像素屏幕抖动持续0.1秒主词
Slide stack在 overflow:hidden 蒙版内 from {yPercent: 110},交错0.06秒,power4.out 0.35秒列表、双行短语
StretchscaleY: 1.6 → 1,transformOrigin: bottom,back.out(3) 0.3秒强调、动词
Letter cascade字母 from {y: -120, rotation: -20, opacity: 0},交错0.025秒,back.out(2)名称、标题
Flip swap旧词 rotationX: 90 退出 (0.12秒),新词 rotationX: -90 → 0 进入 (0.18秒)X变成Y
Split单词两半向 x: ±140 滑开以显示中间较小的单词对比
ExityPercent: -110 或 scale: 0.7, opacity: 0,0.15秒,power2.in每个短语离开

退出要快(0.12–0.18秒),进入要比你想象的更敏捷(0.18–0.35秒)。每个短语落地后至少保持0.35秒,以便阅读。

让它引人注目

  • 充满画框。 根据画框宽度确定字号,而不是根据字号确定尺寸。250像素以上的窄体字体才符合风格。
  • 颜色翻转: 在2–3个关键短语上,背景在深色和浅色(或强调色)之间切换,作为节拍上的硬切。
  • 纹理和深度: 微妙的颗粒叠加层、柔和的阴影或主词后面的偏移副本(强调色,偏移10–14像素),以及偶尔出现的巨大淡化文字背景。
  • 镜头动感: 整个舞台缓慢的2–4%缩放漂移,在主词命中时 punch-in(缩放1.06持续0.1秒),以及猛击时的6–10像素抖动。
  • 带形状的文字: 拉出的下划线、在单词后面弹出的圆圈、箭头。使用强调色,扁平。
  • 以锁定帧结束: 标题或行动号召,完全构图,保持1–1.5秒,匹配开头帧以便循环。
  • 质量门: 如果任何截图显示居中且背景纯白的小字,说明还没完成。

构建:模板

<!doctype html><html><head><meta charset="UTF-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Anton&family=Inter:wght@800&display=swap">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<style>
html,body{margin:0;width:1080px;height:1920px;overflow:hidden;background:#111}
#root{width:100%;height:100%;position:relative;overflow:hidden}
#stage{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.phrase{position:absolute;left:60px;right:120px;top:270px;bottom:670px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:0}
.line{overflow:hidden;line-height:.92;padding:0 8px}
.w{display:inline-block;font-family:Anton,sans-serif;text-transform:uppercase;color:#f4f1ea;white-space:nowrap}
.hero{color:#ff4d2e}
.flip .w{color:#fff}.flip .hero{color:#111}
.small{font-family:Inter,system-ui,sans-serif;font-weight:800;text-transform:none}
#bg{position:absolute;inset:0;background:#111}
</style></head>
<body><div id="root" data-composition-id="main" data-start="0" data-duration="8" data-width="1080" data-height="1920">
<div id="bg"></div><div id="stage"></div></div>
<script>
// [time, lines, moveName, heroWordIndex, flipBackground?]  -- edit this list; times in seconds
const PHRASES = [
  [0.2, ["WORDS"], "slam", 0],
  [1.0, ["can", "MOVE"], "stack", 1],
  [2.0, ["LIKE", "MUSIC"], "stretch", 1, true],
  [3.2, ["one", "idea"], "stack", 1],
  [4.2, ["AT A", "TIME"], "slam", 1],
  [5.4, ["KINETIC", "TYPE"], "cascade", 1],
];
const END = 8, W = 1080 - 180;
// measure text only after the web fonts load, then build and register the timeline
document.fonts.ready.then(() => {
const tl = gsap.timeline({ paused: true });
const stage = document.getElementById("stage");
PHRASES.forEach(([t, lines, move, hero, flip], i) => {
  const p = document.createElement("div"); p.className = "phrase" + (flip ? " flip" : ""); stage.appendChild(p);
  const words = lines.map((txt, k) => {
    const line = document.createElement("div"); line.className = "line";
    const w = document.createElement("span"); w.className = "w" + (k === hero ? " hero" : "") + (txt === txt.toLowerCase() && k !== hero ? " small" : "");
    w.textContent = txt; line.appendChild(w); p.appendChild(line);
    w.style.fontSize = "100px"; const fit = Math.min(420, (100 * W) / w.getBoundingClientRect().width); // fill the width
    w.style.fontSize = (w.classList.contains("small") ? Math.min(fit, 110) : fit) + "px";
    return w;
  });
  gsap.set(p, { autoAlpha: 0 });
  tl.set(p, { autoAlpha: 1 }, t);
  if (move === "slam") tl.from(words, { scale: 2.6, opacity: 0, duration: 0.22, ease: "expo.out", stagger: 0.08 }, t)
                         .fromTo("#stage", { x: -10 }, { x: 10, duration: 0.04, yoyo: true, repeat: 3, ease: "none" }, t + 0.18).set("#stage", { x: 0 }, t + 0.34);
  if (move === "stack") tl.from(words, { yPercent: 110, duration: 0.35, ease: "power4.out", stagger: 0.07 }, t);
  if (move === "stretch") tl.from(words, { scaleY: 1.7, opacity: 0, transformOrigin: "50% 100%", duration: 0.3, ease: "back.out(3)", stagger: 0.08 }, t);
  if (move === "cascade") words.forEach((w, k) => {
    const letters = Array.from(w.textContent); w.textContent = "";
    letters.forEach((c) => { const l = document.createElement("span"); l.style.display = "inline-block"; l.textContent = c; w.appendChild(l); });
    tl.from(w.children, { y: -140, rotation: -20, opacity: 0, duration: 0.4, ease: "back.out(2)", stagger: 0.03 }, t + k * 0.15); });
  const next = PHRASES[i + 1] ? PHRASES[i + 1][0] : null;
  if (flip) tl.set("#bg", { background: "#ff4d2e" }, t).set("#bg", { background: "#111" }, next || END);   // colour flip on a key phrase
  if (next) tl.to(words, { yPercent: -110, opacity: 0, duration: 0.15, ease: "power2.in", stagger: 0.03 }, next - 0.17)
               .set(p, { autoAlpha: 0 }, next);
});
tl.fromTo("#root", { scale: 1 }, { scale: 1.04, duration: END, ease: "none" }, 0);         // slow drift
tl.set({}, {}, END);
window.__timelines["main"] = tl;
});
</script></body></html>

编辑 PHRASES 来更改单词和时间。通过从动作词汇表中添加一个 if (move === ...) 分支来添加动作。如果有画外音,将 <audio> 元素放入根元素,并从其转录中获取短语时间。

暴露给用户的参数

aspect · phrases (time, lines, move, hero) · fontDisplay · fontSupport · colors (bg, text, accent) · tempo or audio · flipPhrases (where the background flips) · endLockup。

避免

  • 淡入和缓慢漂移作为主要动作。这看起来像幻灯片。
  • 屏幕上一次显示超过4个单词或3行。
  • 多种强调色。只使用一种强调色,且仅用于主词。
  • 使用纤细或轻量字体,以及主词的脚本字体。
  • 文字位于9:16画框的底部670像素或顶部270像素内(平台UI会覆盖它们)。
  • 随机或每次渲染不同的变化。每个值都必须来自时间线。

失败模式

表现原因修复
看起来像幻灯片淡入淡出、长时间停留、每次动作都相同从词汇表中变化动作;进入时间低于0.35秒;退出时间0.15秒
像是字幕,而不是设计文字太小,居中在空白的空间中根据画框宽度调整字号;使用窄体字体;250像素以上
混乱、不可读一次出现太多文字或动作屏幕上只显示一个短语;一个主词;落地后保持0.35秒
节拍不准短语时间靠猜测使用转录或节拍时间;命中提前2–3帧
文字被蒙版裁剪字体行高过紧line-height 为0.92–1.0,并在 .line 上添加内边距;检查截图
渲染中字体错误字体未加载在 <head> 中使用Google Fonts <link>;检查截图是否显示窄体字体

最终渲染前验证

npx hyperframes check
npx hyperframes snapshot --at <每个短语落地后0.3秒的时间点,外加一个退出过程中的时间点>
npx hyperframes render -o renders/<name>.mp4
ffprobe -v error -show_entries stream=width,height,r_frame_rate:format=duration -of csv=p=0 renders/<name>.mp4

查看每个截图和 snapshots/contact-sheet.jpg,并确认:

  • 每个短语占宽度的50–80%,并且在手机屏幕上可读
  • 每个短语一个主词,使用强调色或不同的动作
  • 没有任何两个短语在任何帧上重叠
  • 所有文字都在对应宽高比的安全区域内
  • 结尾锁定帧至少保持1秒,并匹配开场
  • 尺寸和持续时间符合要求

最终渲染前自评

对截图册进行1–10分的评分,评估项:钩子(第一秒)、手机屏幕上的可读性、动作质量、构图、准确性,以及“静音测试”(观众能否在无声音的情况下用一句话总结它?)。只修复得分为7或以下的项目,重新截图,并重复直到所有项目都在8分或以上。如果可以,让一个独立的只读审查者进行评分,这样你就不是在给自己的作品打分。

不要改为
在两个繁忙布局之间交叉淡入淡出退出旧内容,然后引入新内容,或者携带一个对象穿过
一次让多个东西移动交错,一次只关注一个

...

[为翻译截断]

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相关技能