Composition 详解:你的第一个视频"剧本"
HyperFrames 2026 终极指南 · 第3章第1节
系列回顾:上一章我们把 HyperFrames 的整体架构扒了个底朝天,知道了从 HTML 到 MP4 的完整链路。从这章开始,我们正式进入实战——先搞定 HyperFrames 里最核心的概念:Composition。
一、Composition 到底是什么?
我打一个你可能秒懂的比方:Composition 就是你视频的"剧本"。
你拍电影得有剧本吧?剧本里写了这场戏几点开始、演多久、演员在哪、台词是什么。Composition 干的就是这个事——它用 HTML 的方式告诉 HyperFrames:这个视频有多长、分辨率多少、每一段内容什么时候出现、持续多久。
我第一次接触 Composition 的时候,觉得它跟 After Effects 里的"合成"概念很像。但仔细一想,区别很大:AE 的 Composition 是二进制格式,你没法用文本编辑器打开看;HyperFrames 的 Composition 就是一个普通的 HTML 文件,你甚至可以用 Git 做版本管理。
Composition 的本质就是一个 HTML 文件,通过特定的 data-* 属性来描述视频的结构和时间线。
二、data-* 属性完整语义详解
HyperFrames 用一组 data-* 属性来标记 HTML 元素在视频中的角色。这些属性就是"剧本"里的标记符号。我按重要程度给你排个序:
2.1 data-composition-id —— 视频的"身份证号"
<div data-composition-id="my-awesome-video">
<!– 整个视频的内容都在这里 –>
</div>
作用:标记这个元素是一个 Composition 的根容器。一个 HTML 文件可以有多个 Composition,靠这个 ID 来区分。
规则:
- 必须是唯一标识符,同一个文件内不能重复
- 建议使用 kebab-case 命名(my-video),不要用驼峰或中文
- 渲染时通过 CLI 指定要渲染哪个 composition
# 渲染指定的 composition
npx hyperframes render ./my-video –composition my-awesome-video
# 如果只有一个 composition,可以省略 –composition 参数
npx hyperframes render ./my-video
踩坑经历:我曾经在一个文件里放了两个 composition,ID 写成了驼峰命名 myVideo,结果 CLI 一直找不到。后来才发现 HyperFrames 内部做 ID 匹配时是严格区分大小写的,而且建议用 kebab-case。这个坑你别踩。
2.2 data-start —— “这段戏什么时候开场”
<div class="clip" data-start="3">
<h1>这段内容从第3秒开始</h1>
</div>
作用:定义这个 clip 在视频时间线上的起始位置,单位是秒。
规则:
- 值必须 >= 0
- 可以是小数,比如 data-start="1.5" 表示从第 1.5 秒开始
- 必须加 class="clip",否则 data-start 不会被识别(这个我后面 Clip 与 Track 那节详细讲)
时间坐标系:HyperFrames 的时间坐标系很简单——从 0 开始,向右递增,单位是秒。
时间轴: 0s 3s 6s 9s
|———|———|———|
├─ 背景 ──────────────────────┤
├── Clip A ──┤
├── Clip B ──┤
2.3 data-duration —— “这段戏演多久”
<div class="clip" data-start="3" data-duration="5">
<h1>从第3秒开始,持续5秒</h1>
</div>
作用:定义 clip 的持续时间,单位也是秒。
规则:
- 值必须 > 0
- data-start + data-duration 不能超过 composition 的总时长
- 如果超出,linter 会报错
一个容易犯的错误:很多人以为 data-duration 是"结束时间"。不是的!它是"持续时长"。如果你的 clip 从第 3 秒开始,要到第 8 秒结束,那 data-duration 应该写 5(8 – 3 = 5),不是 8。
2.4 data-track-index —— “这段戏在哪个舞台”
<div class="clip" data-start="0" data-duration="10" data-track-index="0">
<div class="background">背景层</div>
</div>
<div class="clip" data-start="2" data-duration="6" data-track-index="1">
<div class="subtitle">字幕层</div>
</div>
作用:定义 clip 所在的轨道编号。轨道从 0 开始,数字越大越在上面(类似 Photoshop 的图层)。
规则:
- 必须是整数,从 0 开始
- 同一轨道内的 clip 不能有时间重叠(同一时间只能显示一个)
- 不同轨道的 clip 可以同时显示(叠加效果)
- 如果不指定 data-track-index,默认为 0
这个概念太重要了,我单独用一整节来讲。现在你只要知道:轨道就是视频的"层",用来管理多个内容块的叠加和切换。
2.5 data-composition-src —— “引用外部剧本”
<!– 主文件 main.html –>
<div data-composition-id="full-video" data-duration="30">
<!– 引用另一个 composition 文件 –>
<div data-composition-src="./intro.html"
data-start="0"
data-duration="5">
</div>
<div data-composition-src="./main-content.html"
data-start="5"
data-duration="20">
</div>
<div data-composition-src="./outro.html"
data-start="25"
data-duration="5">
</div>
</div>
作用:引用外部的 composition 文件,实现模块化管理。类似于编程里的 import。
规则:
- 路径相对于当前文件
- 被引用的文件本身也必须是合法的 composition
- 引用时可以覆盖被引用 composition 的部分属性
什么时候用? 当你的视频很长、结构复杂时,把所有内容塞在一个 HTML 文件里会很痛苦。用 data-composition-src 可以把视频拆成多个模块:
project/
├── main.html # 主编排文件
├── scenes/
│ ├── intro.html # 片头
│ ├── chapter1.html # 第一章
│ ├── chapter2.html # 第二章
│ └── outro.html # 片尾
├── assets/
│ ├── style.css
│ └── animations.js
└── meta.json # 配置文件
三、一个完整的最小可运行 Composition 文件
光说属性太抽象了,我直接给你一个完整能跑的例子,逐行给你讲:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的第一个 Composition</title>
<style>
/* ① 根容器尺寸 = 视频分辨率 */
[data-composition-id] {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #0a0a0a;
font-family: 'PingFang SC', sans-serif;
}
/* ② clip 默认隐藏,由 HyperFrames 控制显隐 */
.clip {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
/* ③ 各 clip 的样式 */
.clip-title {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.clip-title h1 {
color: white;
font-size: 80px;
font-weight: 700;
text-align: center;
}
.clip-content {
background: #1a1a2e;
color: #eee;
font-size: 40px;
padding: 60px;
}
.clip-end {
background: #16213e;
color: #e94560;
font-size: 60px;
font-weight: bold;
}
</style>
</head>
<body>
<!– ④ Composition 根容器 –>
<div data-composition-id="hello-world"
data-duration="12">
<!– ⑤ Clip 1:标题卡,0-4秒 –>
<div class="clip clip-title"
data-start="0"
data-duration="4"
data-track-index="0">
<h1>Hello<br>HyperFrames</h1>
</div>
<!– ⑥ Clip 2:内容展示,4-9秒 –>
<div class="clip clip-content"
data-start="4"
data-duration="5"
data-track-index="0">
<p>用 HTML 写视频,就是这么简单。</p>
<p>你写的每一行 CSS,都会变成视频画面。</p>
</div>
<!– ⑦ Clip 3:结束卡,9-12秒 –>
<div class="clip clip-end"
data-start="9"
data-duration="3"
data-track-index="0">
<p>Thanks for Watching!</p>
</div>
</div>
</body>
</html>
逐行解析:
| ① | [data-composition-id] { width: 1920px; height: 1080px; } | 根容器尺寸决定了输出视频的分辨率。1920×1080 就是 1080p |
| ② | .clip { position: absolute; } | 所有 clip 用绝对定位,这样它们才能叠加在同一位置 |
| ③ | .clip-title { … } | 每个 clip 的视觉样式,跟写普通网页一模一样 |
| ④ | data-composition-id="hello-world" | 标记这是 composition 根容器,ID 是 “hello-world” |
| ④ | data-duration="12" | 视频总时长 12 秒 |
| ⑤ | data-start="0" data-duration="4" | 第一个 clip 从 0 秒开始,持续 4 秒 |
| ⑥ | data-start="4" data-duration="5" | 第二个 clip 从 4 秒开始,持续 5 秒 |
| ⑦ | data-start="9" data-duration="3" | 第三个 clip 从 9 秒开始,持续 3 秒 |
注意看时间线:
0s 4s 9s 12s
|───────────|───────────|──────────|
│ Clip 1 │ Clip 2 │ Clip 3 │
│ (标题卡) │ (内容) │ (结束卡) │
│ track 0 │ track 0 │ track 0 │
三个 clip 都在 track 0 上,首尾相接,无缝衔接。这就是最简单的"场景切换"。
把这段代码保存为 hello-world.html,然后跑:
npx hyperframes render ./hello-world.html –output hello.mp4
你就能得到一个 12 秒的 1080p 视频。
四、meta.json 配置详解
除了 HTML 里的 data-* 属性,HyperFrames 还支持一个 meta.json 配置文件来定义 composition 的全局参数。
{
"compositions": [
{
"id": "hello-world",
"width": 1920,
"height": 1080,
"fps": 30,
"duration": 12,
"background": "#0a0a0a",
"assets": {
"css": ["./assets/style.css"],
"js": ["./assets/animations.js"]
}
}
]
}
4.1 各字段说明
| id | string | ✅ | Composition ID,对应 HTML 里的 data-composition-id |
| width | number | ✅ | 视频宽度(像素) |
| height | number | ✅ | 视频高度(像素) |
| fps | number | ❌ | 帧率,默认 30 |
| duration | number | ✅ | 总时长(秒) |
| background | string | ❌ | 背景色,CSS 颜色值 |
| assets.css | string[] | ❌ | 额外引入的 CSS 文件 |
| assets.js | string[] | ❌ | 额外引入的 JS 文件 |
4.2 HTML 属性 vs meta.json,到底用哪个?
这是个很多人问我的问题。我的建议是:
| 简单的单文件视频 | 直接用 HTML 的 data-* 属性 |
| 多文件组合的复杂项目 | 用 meta.json 统一管理 |
| 需要动态参数(分辨率、帧率) | 用 meta.json,方便脚本修改 |
| 团队协作、CI/CD | 用 meta.json,配置和代码分离 |
实际上两者可以混用。meta.json 里的配置会作为默认值,HTML 里的 data-* 属性可以覆盖它。优先级是:HTML data- > meta.json > 默认值*。
五、视频分辨率、帧率、时长的配置方式
这三个参数是视频最基础的规格,HyperFrames 提供了多种配置方式:
5.1 分辨率
<!– 方式1:CSS 直接设置(推荐) –>
<style>
[data-composition-id] {
width: 1920px;
height: 1080px;
}
</style>
<!– 方式2:meta.json –>
<!– { "width": 1920, "height": 1080 } –>
<!– 方式3:CLI 参数覆盖 –>
<!– npx hyperframes render ./video –width 1280 –height 720 –>
常用分辨率速查表:
| 4K | 3840×2160 | 高端展示、大屏播放 |
| 1080p | 1920×1080 | 标准高清,最常用 |
| 720p | 1280×720 | 网络传输、快速预览 |
| 竖屏 1080×1920 | 1080×1920 | 抖音/快手/Reels |
| 正方形 | 1080×1080 | 社交媒体方形视频 |
| 4:3 | 1440×1080 | 复古风格 |
5.2 帧率
<!– 方式1:meta.json(推荐) –>
<!– { "fps": 60 } –>
<!– 方式2:CLI 参数 –>
<!– npx hyperframes render ./video –fps 60 –>
帧率选择指南:
| 24 | 电影感 | 电影风格视频 |
| 30 | 流畅 | 网络视频、教程(默认值) |
| 60 | 丝滑 | 动画展示、游戏录屏 |
| 12 | 定格动画感 | 特殊艺术风格 |
5.3 时长
<!– 方式1:HTML data-duration(最直观) –>
<div data-composition-id="my-video" data-duration="30">
<!– 方式2:meta.json –>
<!– { "duration": 30 } –>
时长计算小技巧:视频的总时长应该等于最后一个 clip 的 data-start + data-duration。如果你设置了 data-duration="30" 但最后一个 clip 在第 25 秒就结束了,那后面 5 秒就是黑屏(或者背景色)。
六、Composition 的结构层次
用一张图来展示 Composition 内部的层次关系:
#mermaid-svg-x4pYzKF5cwtra0uh{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-x4pYzKF5cwtra0uh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-x4pYzKF5cwtra0uh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-x4pYzKF5cwtra0uh .error-icon{fill:#552222;}#mermaid-svg-x4pYzKF5cwtra0uh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-x4pYzKF5cwtra0uh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-x4pYzKF5cwtra0uh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-x4pYzKF5cwtra0uh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-x4pYzKF5cwtra0uh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-x4pYzKF5cwtra0uh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-x4pYzKF5cwtra0uh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-x4pYzKF5cwtra0uh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-x4pYzKF5cwtra0uh .marker.cross{stroke:#333333;}#mermaid-svg-x4pYzKF5cwtra0uh svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-x4pYzKF5cwtra0uh p{margin:0;}#mermaid-svg-x4pYzKF5cwtra0uh .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-x4pYzKF5cwtra0uh .cluster-label text{fill:#333;}#mermaid-svg-x4pYzKF5cwtra0uh .cluster-label span{color:#333;}#mermaid-svg-x4pYzKF5cwtra0uh .cluster-label span p{background-color:transparent;}#mermaid-svg-x4pYzKF5cwtra0uh .label text,#mermaid-svg-x4pYzKF5cwtra0uh span{fill:#333;color:#333;}#mermaid-svg-x4pYzKF5cwtra0uh .node rect,#mermaid-svg-x4pYzKF5cwtra0uh .node circle,#mermaid-svg-x4pYzKF5cwtra0uh .node ellipse,#mermaid-svg-x4pYzKF5cwtra0uh .node polygon,#mermaid-svg-x4pYzKF5cwtra0uh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-x4pYzKF5cwtra0uh .rough-node .label text,#mermaid-svg-x4pYzKF5cwtra0uh .node .label text,#mermaid-svg-x4pYzKF5cwtra0uh .image-shape .label,#mermaid-svg-x4pYzKF5cwtra0uh .icon-shape .label{text-anchor:middle;}#mermaid-svg-x4pYzKF5cwtra0uh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-x4pYzKF5cwtra0uh .rough-node .label,#mermaid-svg-x4pYzKF5cwtra0uh .node .label,#mermaid-svg-x4pYzKF5cwtra0uh .image-shape .label,#mermaid-svg-x4pYzKF5cwtra0uh .icon-shape .label{text-align:center;}#mermaid-svg-x4pYzKF5cwtra0uh .node.clickable{cursor:pointer;}#mermaid-svg-x4pYzKF5cwtra0uh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-x4pYzKF5cwtra0uh .arrowheadPath{fill:#333333;}#mermaid-svg-x4pYzKF5cwtra0uh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-x4pYzKF5cwtra0uh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-x4pYzKF5cwtra0uh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-x4pYzKF5cwtra0uh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-x4pYzKF5cwtra0uh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-x4pYzKF5cwtra0uh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-x4pYzKF5cwtra0uh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-x4pYzKF5cwtra0uh .cluster text{fill:#333;}#mermaid-svg-x4pYzKF5cwtra0uh .cluster span{color:#333;}#mermaid-svg-x4pYzKF5cwtra0uh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-x4pYzKF5cwtra0uh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-x4pYzKF5cwtra0uh rect.text{fill:none;stroke-width:0;}#mermaid-svg-x4pYzKF5cwtra0uh .icon-shape,#mermaid-svg-x4pYzKF5cwtra0uh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-x4pYzKF5cwtra0uh .icon-shape p,#mermaid-svg-x4pYzKF5cwtra0uh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-x4pYzKF5cwtra0uh .icon-shape .label rect,#mermaid-svg-x4pYzKF5cwtra0uh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-x4pYzKF5cwtra0uh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-x4pYzKF5cwtra0uh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-x4pYzKF5cwtra0uh :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Composition 根容器data-composition-iddata-duration
Track 0 (背景轨道)data-track-index=0
Track 1 (内容轨道)data-track-index=1
Track 2 (特效轨道)data-track-index=2
Clip Adata-start=0data-duration=10背景视频/图片
Clip Bdata-start=10data-duration=10切换场景
Clip Cdata-start=2data-duration=6字幕/标题
Clip Ddata-start=12data-duration=5说明文字
Clip Edata-start=0data-duration=3转场动画
Clip Fdata-start=10data-duration=2转场动画
层次关系解读:
渲染时,HyperFrames 会从左到右(track 0 → track N)逐层渲染,后渲染的层会覆盖先渲染的层。这就像 Photoshop 的图层——你在最上面的图层画的东西,会遮住下面图层的內容。
七、多 Composition 管理
一个项目里经常有多个 composition。比如你要做一系列产品介绍视频,每个产品的模板一样但内容不同:
<!– templates.html –>
<!– 模板 A:15秒版本 –>
<div data-composition-id="product-intro-15s" data-duration="15">
<div class="clip" data-start="0" data-duration="5">
<h1>产品名</h1>
</div>
<div class="clip" data-start="5" data-duration="7">
<p>产品特点介绍</p>
</div>
<div class="clip" data-start="12" data-duration="3">
<p>立即购买</p>
</div>
</div>
<!– 模板 B:30秒版本 –>
<div data-composition-id="product-intro-30s" data-duration="30">
<div class="clip" data-start="0" data-duration="5">
<h1>产品名</h1>
</div>
<div class="clip" data-start="5" data-duration="15">
<p>详细功能演示</p>
</div>
<div class="clip" data-start="20" data-duration="7">
<p>用户评价</p>
</div>
<div class="clip" data-start="27" data-duration="3">
<p>立即购买</p>
</div>
</div>
# 渲染 15 秒版本
npx hyperframes render ./templates.html –composition product-intro-15s
# 渲染 30 秒版本
npx hyperframes render ./templates.html –composition product-intro-30s
# 批量渲染所有 composition
npx hyperframes render ./templates.html –all
八、Composition 的嵌套与引用
前面提到了 data-composition-src,这里给一个更实际的例子:
<!– main.html – 年度总结视频 –>
<div data-composition-id="annual-review-2025" data-duration="180">
<!– 片头:0-10秒 –>
<div data-composition-src="./scenes/opening.html"
data-start="0" data-duration="10"
data-track-index="0">
</div>
<!– Q1 回顾:10-50秒 –>
<div data-composition-src="./scenes/q1.html"
data-start="10" data-duration="40"
data-track-index="0">
</div>
<!– Q2 回顾:50-90秒 –>
<div data-composition-src="./scenes/q2.html"
data-start="50" data-duration="40"
data-track-index="0">
</div>
<!– Q3 回顾:90-130秒 –>
<div data-composition-src="./scenes/q3.html"
data-start="90" data-duration="40"
data-track-index="0">
</div>
<!– Q4 回顾:130-170秒 –>
<div data-composition-src="./scenes/q4.html"
data-start="130" data-duration="40"
data-track-index="0">
</div>
<!– 片尾:170-180秒 –>
<div data-composition-src="./scenes/ending.html"
data-start="170" data-duration="10"
data-track-index="0">
</div>
</div>
这样做的好处:
- 模块化:每个季度是独立文件,各团队可以并行制作
- 可复用:opening 和 ending 模板可以复用到其他视频
- 易维护:改一个季度不影响其他季度
- 渲染快:可以只渲染修改过的部分
九、常见错误排查
我整理了一份 Composition 新手最常犯的错误清单:
| 忘记 class="clip" | clip 不显示 | HyperFrames 只识别带 clip class 的元素 | 加上 class="clip" |
| data-start 写成结束时间 | clip 出现时间不对 | data-start 是开始时间,不是结束时间 | 改为 开始时间 = 结束时间 – 持续时长 |
| 根容器没设尺寸 | 渲染出来黑屏 | 没有告诉 HyperFrames 视频分辨率 | 在 CSS 里设置 width 和 height |
| clip 时间超出总时长 | linter 报错 | start + duration > composition duration | 调整 clip 时间或增加总时长 |
| 同轨道 clip 时间重叠 | 渲染异常 | 同一 track 的 clip 不能重叠 | 调整时间或分到不同 track |
| data-composition-id 重复 | 只渲染第一个 | ID 必须唯一 | 改不同的 ID |
小结
今天我们把 Composition 这个核心概念彻底搞明白了。总结一下关键知识点:
Composition 搞明白了,你就掌握了 HyperFrames 的"语言"。后面不管是做简单的文字动画还是复杂的多轨道视频,都是在写 Composition。
下节预告
这节我们讲了 Composition 的整体结构和属性语义,但有一个关键概念只是提了一下没展开——Clip 与 Track。下一节我们就深入这个话题:为什么每个定时元素都需要 class="clip"?多轨道到底怎么管理?我会用一个 3 轨道视频的完整示例,把时间线管理这件事讲透。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!本系列持续更新中,关注不迷路~




