HyperFrames
HTML 是视频的真相源。一个合成就是一个 HTML 文件,其中包含用于时间控制的 data-* 属性、一个用于动画的 GSAP 时间线,以及用于外观的 CSS。该框架负责处理剪辑可见性、媒体播放和时间线同步。
方法
探索(仅限探索性请求)
对于开放式的请求(例如“制作一个产品发布视频”、“为我们的品牌创作一些内容”),当用户尚未确定方向时,在挑选颜色之前先了解意图:
- 受众 — 谁在看这个?开发者?高管?普通消费者?
- 平台 — 在哪里播放?社交媒体(15秒)、网站主屏、产品演示、内部使用?
- 优先级 — 什么最重要?动画质量?内容准确性?品牌保真度?速度?
- 变体 — 用户想要多个选项,还是单一的最佳方案?
对于具体的请求(例如“添加一个标题卡片”、“修复场景3的时间”),跳过探索步骤。
对于探索性请求,考虑提供 2-3 个有实质性区别的变体——不仅仅是颜色替换,而是不同的节奏、能量水平或结构方法。一个保守/可预期,一个大胆。这不是强制性的——只是适当时可用的一种工具。
步骤 1:设计系统
如果项目中存在 design.md 或 DESIGN.md 文件,请先读取它(检查两种大小写——它们在 Linux 上是不同的文件)。这是品牌颜色、字体和约束的真相源。使用其精确的值——不要发明颜色或替代字体。任何格式都可以(YAML 前置元数据、散文、表格——只需提取值)。
如果它指定的字体在本地找不到(没有包含 .woff2 文件的 fonts/ 目录,也不是内置字体),请在编写 HTML 之前警告用户:“design.md 指定了 [字体名称],但未找到字体文件。请将 .woff2 文件添加到 fonts/ 目录,否则我将回退到 [最接近的内置替代字体]。”
如果不存在 design.md,则给用户提供选择:
- 用户指定了风格或氛围? → 阅读 visual-styles.md 中的 8 个命名预设。选择最接近的匹配。
- 想可视化浏览选项? → 运行设计选择器:阅读 references/design-picker.md 了解完整工作流程。这将提供一个可视化选择器页面。用户在浏览器中配置氛围、调色板、排版和动画,然后复制生成的 design.md 并粘贴回对话中。
- 想跳过并快速进行? → 询问:氛围、浅色或深色、是否有品牌颜色/字体?然后从 house-style.md 中选择一个调色板。
design.md 定义了品牌。它不定义视频合成规则。 这些规则来自 references/video-composition.md 和 house-style.md。在视频合适的比例下使用品牌颜色——而不是以 Web 界面的不透明度使用。
步骤 2:提示扩展
对于每个合成(单场景片段和琐碎编辑除外)总是执行此步骤。此步骤将用户的意图与 design.md 和 house-style.md 结合起来,并产生一个一致性的中间成果,所有下游智能体都能以相同方式解读。
阅读 references/prompt-expansion.md 了解完整过程和输出格式。
步骤 3:规划
在编写 HTML 之前,从高层次思考:
- 什么 — 观众应该体验到什么?确定叙事弧线、关键时刻和情感节拍。
- 结构 — 有多少个合成,哪些是子合成 vs 内联,哪些轨道承载什么(视频、音频、叠加层、字幕)。
- 节奏 — 在实现之前声明你的场景节奏。哪些场景是快速击打,哪些是保持,着色器落在哪里,能量峰值在哪里。命名模式:fast-fast-SLOW-fast-SHADER-hold。阅读 references/beat-direction.md 了解节奏模板。
- 时间安排 — 哪些剪辑驱动持续时间,过渡落在哪里,节奏如何。
- 布局 — 首先构建最终状态。见下方的“先布局后动画”。
- 动画 — 然后使用以下规则添加运动。
按要求构建。 请求“一个标题卡片”并不意味着“一个标题卡片 + 3 个支持场景 + 环境音乐 + 字幕”。每个场景、每个元素、每个补间动画都应有其存在的理由。如果额外的场景或元素能真正提升作品,请提出建议——但不要擅自添加。
对于小型编辑(修正颜色、调整时间、添加一个元素),直接跳到规则部分。
<硬性门控>
在编写任何合成 HTML 之前——请确认你已经从步骤 1 获得了视觉标识。如果你正在使用 #333、#3b82f6 或 Roboto,那么你跳过了该步骤。
</硬性门控>
先布局后动画
将每个元素定位在其最显眼时刻所在的位置——即它完全进入、正确放置且尚未退出的那一帧。首先将其编写为静态 HTML+CSS。暂不使用 GSAP。
为什么这很重要: 如果你将元素定位在动画起始状态(屏幕外、缩放为 0、透明度为 0)并通过补间动画将其移动到你认为应该落地的位置,那么你是在猜测最终的布局。重叠问题在视频渲染之前是不可见的。通过先构建最终状态,你可以在添加任何运动之前发现并修复布局问题。
过程
- 为每个场景确定主帧——即大多数元素同时可见的时刻。这就是你要构建的布局。
- 为该帧编写静态 CSS。
.scene-content容器必须使用width: 100%; height: 100%; padding: Npx;并配合display: flex; flex-direction: column; gap: Npx; box-sizing: border-box来完全填充场景。使用内边距将内容向内推——绝不要对内容容器使用position: absolute; top: Npx。绝对定位的内容容器在内容高于剩余空间时会溢出。position: absolute仅保留给装饰性元素使用。 - 使用
gsap.from()添加入场动画——从屏幕外/不可见状态动画到 CSS 位置。CSS 位置是真实基准;补间动画描述了到达那里的过程。(在通过data-composition-src加载的子合成中,优先使用gsap.fromTo()——请参阅 references/motion-principles.md 中的负载关键 GSAP 规则。) - 使用
gsap.to()添加退场动画——从 CSS 位置动画到屏幕外/不可见状态。
示例
/* scene-content 填充场景,内边距定位内容 */
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
padding: 120px 160px;
gap: 24px;
box-sizing: border-box;
}
.title {
font-size: 120px;
}
.subtitle {
font-size: 42px;
}
/* 容器填充任何场景尺寸(1920x1080、1080x1920 等)。
内边距定位内容。Flex + gap 处理间距。 */
错误——硬编码尺寸和绝对定位:
.scene-content {
position: absolute;
top: 200px;
left: 160px;
width: 1920px;
height: 1080px;
display: flex; /* ... */
}
// 步骤 3:动画进入这些位置
tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);
// 步骤 4:从这些位置动画退出
tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);
当元素在时间上共享空间时
如果元素 A 在元素 B 进入同一区域之前退出,则两者都应该针对各自的主帧具有正确的 CSS 位置。时间线顺序保证它们永远不会在视觉上共存——但如果你跳过了布局步骤,你将无法发现由于时间错误而导致的意外重叠情况。
什么算作有意重叠
分层效果(文字后的发光、阴影元素、背景图案)和 Z 轴堆叠设计(卡片堆叠、深度层)是故意的。布局步骤的目的是捕捉意外重叠——两个标题重叠在一起、一个统计数字覆盖了标签、内容溢出画框。
数据属性
所有剪辑
| 属性 | 必需 | 值 |
|---|---|---|
id | 是 | 唯一标识符 |
data-start | 是 | 秒数或剪辑 ID 引用("el-1"、"intro + 2") |
data-duration | 对于 img/div/合成 是必需的 | 秒数。视频/音频默认使用媒体持续时间。 |
data-track-index | 是 | 整数。同一轨道的剪辑不能重叠。 |
data-media-start | 否 | 从源文件的裁剪偏移量(秒) |
data-volume | 否 | 0-1(默认为 1) |
data-track-index 不影响视觉分层——请使用 CSS z-index。
合成剪辑
| 属性 | 必需 | 值 |
|---|---|---|
data-composition-id | 是 | 唯一的合成 ID |
data-start | 是 | 开始时间(根合成:使用 "0") |
data-duration | 是 | 优先于 GSAP 时间线持续时间 |
data-width / data-height | 是 | 像素尺寸(1920x1080 或 1080x1920) |
data-composition-src | 否 | 外部 HTML 文件的路径 |
data-variable-values | 否 | 子合成宿主元素上每个实例的变量覆盖 JSON 对象 |
在根 <html> 元素上:
| 属性 | 必需 | 值 |
|---|---|---|
data-composition-variables | 否 | 已声明变量的 JSON 数组(id/type/label/default)——驱动 Studio 编辑 UI,并为 getVariables() 提供默认值 |
合成结构
通过 data-composition-src 加载的子合成使用 <template> 包装器。独立合成(主 index.html)不使用 <template>——它们直接将在 <body> 中放置 data-composition-id 的 div。在独立文件上使用 <template> 会隐藏所有内容,导致浏览器渲染失败。
子合成结构:
<template id="my-comp-template">
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
<!-- 内容 -->
<style>
[data-composition-id="my-comp"] {
/* 作用域样式 */
}
</style>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// 补间动画...
window.__timelines["my-comp"] = tl;
</script>
</div>
</template>
在根组合中加载:<div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>
变量(参数化合成)
使用不同的内容渲染同一个合成——标题、主题颜色、价格、字幕——而无需编辑源 HTML。
三步模式:
- 声明 合成
<html>根上的变量,使用data-composition-variables。每个条目需要id、type(string、number、color、boolean、enum之一)、label和default。枚举条目还需要options: [{value, label}, ...]。 - 读取 合成脚本内已解析的值,使用
window.__hyperframes.getVariables()。返回声明默认值 + 每个实例覆盖 + CLI 覆盖的合并结果。 - 覆盖 在渲染时使用
npx hyperframes render --variables '{...}'(顶层)或使用宿主元素上的data-variable-values='{...}'(子合成的每个实例)。
<!doctype html>
<html
data-composition-variables='[
{"id":"title","type":"string","label":"标题","default":"你好"},
{"id":"theme","type":"enum","label":"主题","default":"light","options":[
{"value":"light","label":"浅色"},
{"value":"dark","label":"深色"}
]}
]'
>
<body>
<div data-composition-id="root" data-width="1920" data-height="1080">
<h1 id="hero" class="clip" data-start="0" data-duration="3"></h1>
<script>
const { title, theme } = window.__hyperframes.getVariables();
document.getElementById("hero").textContent = title;
document.body.dataset.theme = theme;
</script>
</div>
</body>
</html>
# 开发预览使用声明的默认值
npx hyperframes preview
# 使用覆盖渲染
npx hyperframes render --variables '{"title":"Q4 报告","theme":"dark"}' --output q4.mp4
# 或者从 JSON 文件
npx hyperframes render --variables-file ./vars.json
子合成每个实例的值: 相同的 getVariables() 在通过 data-composition-src 加载的子合成内也能正常工作。每个宿主元素传递自己的值:
<div
data-composition-id="card-pro"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"专业版","price":"$29"}'
></div>
<div
data-composition-id="card-enterprise"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"企业版","price":"$99"}'
></div>
渲染到 MP4
# 单文件(自动发现合成,输出 timing.json + index.mp4)
npx hyperframes render
# 指定合成
npx hyperframes render --composition root
# 分辨率覆盖
npx hyperframes render --overrides '{"width":1920,"height":1080}'
--output 是目录路径(不是文件名)。框架会生成 timing.json 和命名的 .mp4 文件。要自定义 FFmpeg 选项,请使用 --ffmpeg-args。
时间线与媒体管理
时间线文件 (timing.json)
在渲染过程中自动生成。包含每个剪辑在输出视频中的开始/结束时间。用于将字幕或叠加层与渲染的输出同步。
[
{"name":"mouth-move","start":0.0,"end":3.5},
{"name":"slide-01","start":4.0,"end":8.0}
]
媒体文件 (media/ 目录)
将所有源视频和音频文件放在 media/ 中。使用 src="media/voiceover.wav" 引用它们。该目录可由其他人编辑。如果媒体文件包含多个轨道(带画外音的视频、带音乐的画外音),请先预处理:
- 使用
ffmpeg -i input.mp4 -map 0:a:0 audio.mp3提取音频轨道 - 使用 Audition 或 DaVinci Resolve 进行专业混音
- 或者使用
hyperframes-media技能中记录的npx hyperframes audio split命令
常见陷阱
- 缺少
data-media-start作为裁剪:如果你想在视频剪辑中途开始,请设置data-media-start,而不是使用负的偏移量或调整data-start。 - GSAP 的
opacity: 0与可见性:opacity: 0的元素仍然存在并影响布局。它们也可以被点击。对于真正的隐藏,请添加pointer-events: none。该框架依赖可见性来切换剪辑——不要在剪辑元素上使用display: none。 - 剪辑顺序与时间重叠:同一轨道(
data-track-index)上的剪辑不能重叠。如果你得到双倍音频,请检查轨道索引。剪辑的视觉重叠由 CSS 控制,而不是轨道索引。
工作流命令
此技能专注于 HTML 合成创作。对于相关的 CLI 命令:
| 命令集 | 用途 |
|---|---|
hyperframes-cli (vskill) | init、lint、inspect、preview、render |
hyperframes-media (mskill) | tts、transcribe、remove-background |