HyperFrames 是做什么的?
HTML 是视频的真理源。一个合成(Composition)是一个包含 data-* 属性用于时间控制、GSAP 时间线用于动画、CSS 用于界面的 HTML 文件。该框架负责处理剪辑可见性、媒体播放和时间线同步。
方法论
在编写 HTML 之前,请从高层思考:
- 什么 — 观看者应该体验什么?确定叙事弧线、关键时刻和情感节奏。
- 结构 — 需要多少个合成?哪些是子合成,哪些是内联的?哪些轨道承载什么内容(视频、音频、叠加层、字幕)。
- 时序 — 哪些剪辑决定时长?过渡在何处发生?节奏如何?
- 布局 — 先构建最终状态。参见下面的“先布局后动画”。
- 动画 — 然后按照下面的规则添加运动。
对于小修改(修复颜色、调整时间、添加一个元素),直接跳到规则部分。
视觉标识关卡
按此顺序检查:
- 项目中是否存在
DESIGN.md? → 读取它。使用其确切的颜色、字体、运动规则和“不要做什么”约束。 - 是否存在
visual-style.md? → 读取它。应用其style_prompt_full和结构化字段。(注意:visual-style.md是项目特定文件。visual-styles.md是包含 8 个命名预设的风格库 — 两个不同的文件。) - 用户指定了样式(例如“Swiss Pulse”、“黑暗科技风”、“奢侈品牌”)? → 读取 visual-styles.md 中的 8 个命名预设。生成一个最小的
DESIGN.md,包含:## Style Prompt(一段)、## Colors(3-5 个十六进制值及其角色)、## Typography(1-2 个字体族)、## What NOT to Do(3-5 个反模式)。 - 以上都没有? → 在编写任何 HTML 之前,先问 3 个问题:
- 情绪是什么?(爆发/电影感/流畅/技术感/混沌/温暖)
- 浅色还是深色画布?
- 是否有任何特定的品牌色、字体或视觉参考?
然后根据答案生成一个最小的
DESIGN.md。
每个合成都必须将其调色板和排版溯源到 DESIGN.md、visual-style.md 或明确的用户指示。如果您要使用 #333、#3b82f6 或 Roboto — 则说明您跳过了此步骤。
关于运动默认值、尺寸、入场模式和缓动 — 遵循 house-style.md。house-style 处理事物如何移动。DESIGN.md 处理事物的外观。
先布局后动画
将每个元素放置在最醒目的瞬间应处的位置——即元素完全进入、正确放置且尚未退出的那一帧。先将其编写为静态 HTML+CSS。暂不使用 GSAP。
为什么这很重要: 如果您将元素定位在动画起始状态(屏幕外、缩放至 0、不透明度为 0)然后补间到您认为它们应到达的位置,您实际上是在猜测最终布局。重叠问题只有在视频渲染时才会显现。通过先构建最终状态,您可以在添加任何运动之前发现并修复布局问题。
流程
- 识别每个场景的 Hero 帧 — 最多元素同时可见的时刻。这就是您要构建的布局。
- 为该帧编写静态 CSS。
.scene-content容器必须使用width: 100%; height: 100%; padding: Npx;配合display: flex; flex-direction: column; gap: Npx; box-sizing: border-box来填满整个场景。使用 padding 将内容向内推——绝对不要使用position: absolute; top: Npx来定位内容容器。绝对定位的内容容器在内容高于剩余空间时会溢出。将position: absolute仅保留给装饰性元素。 - 使用
gsap.from()添加入场动画 — 从屏幕外/不可见状态动画到 CSS 位置。CSS 位置是真理源;补间描述了到达那里的过程。 - 使用
gsap.to()添加出场动画 — 从 CSS 位置动画到屏幕外/不可见状态。
示例
/* scene-content 填充场景,padding 定位内容 */
.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 等)。
Padding 定位内容。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 进入同一区域之前退出,则两者在其各自的 Hero 帧中应具有正确的 CSS 位置。时间线顺序保证了它们永远不会在视觉上共存——但如果您跳过布局步骤,您将无法发现由于时序错误导致的意外重叠情况。
什么算作有意重叠
分层效果(文字后的发光、阴影元素、背景图案)和 Z 轴堆叠设计(卡片堆叠、深度层)是有意为之的。布局步骤的目的是发现意外重叠——两个标题重叠在一起、统计数字覆盖标签、内容溢出画面。
Data Attributes(数据属性)
所有剪辑
| 属性 | 是否必须 | 值 |
|---|---|---|
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-composition-src 加载的子合成都使用 <template> 包裹器。独立合成(即主 index.html)不使用 <template>——它们将 data-composition-id div 直接放在 <body> 中。在独立文件上使用 <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>
视频和音频
视频必须使用 muted playsinline。音频始终是一个独立的 <audio> 元素:
<video
id="el-v"
data-start="0"
data-duration="30"
data-track-index="0"
src="video.mp4"
muted
playsinline
></video>
<audio
id="el-a"
data-start="0"
data-duration="30"
data-track-index="2"
src="video.mp4"
data-volume="1"
></audio>
时间线契约
- 所有时间线都以
{ paused: true }启动——播放器控制播放 - 注册每条时间线:
window.__timelines["<composition-id>"] = tl - 框架自动嵌套子时间线——请勿手动添加它们
- 时长来自
data-duration,而不是 GSAP 时间线长度 - 切勿创建空补间来设置时长
规则(不可协商)
确定性: 无 Math.random()、Date.now() 或基于时间的逻辑。如果需要伪随机值,请使用种子 PRNG(例如 mulberry32)。
GSAP: 仅动画化视觉属性(opacity、x、y、scale、rotation、color、backgroundColor、borderRadius、变换)。请勿动画化 visibility、display 或调用 video.play()/audio.play()。
动画冲突: 切勿同时从多个时间线动画化同一元素的同一属性。
无 repeat: -1: 无限重复时间线会破坏捕获引擎。根据合成时长计算精确的重复次数:repeat: Math.ceil(duration / cycleDuration) - 1。
同步时间线构建: 切勿在 async/await、setTimeout 或 Promise 内部构建时间线。捕获引擎在页面加载后同步读取 window.__timelines。字体由编译器嵌入,因此可立即使用——无需等待字体加载。
切勿执行以下操作:
- 忘记
window.__timelines注册 - 使用视频作为音频——始终使用静音视频 + 独立的
<audio> - 将视频嵌套在带时间控制的 div 内——使用非时间控制的包裹器
- 使用
data-layer(改用data-track-index)或data-end(改用data-duration) - 对视频元素尺寸进行动画——对包裹器 div 进行动画
- 调用媒体的 play/pause/seek——框架拥有播放控制权
- 创建没有
data-composition-id的顶级容器 - 在任何时间线或补间上使用
repeat: -1——始终使用有限重复 - 异步构建时间线(在
async、setTimeout、Promise内部) - 在后续场景的剪辑元素上使用
gsap.set()——它们在页面加载时不存在于 DOM 中。请改用tl.set(selector, vars, timePosition),并确保时间位置在剪辑的data-start时间或之后。 - 在内容文本中使用
<br>——强制换行应使用 CSS。