OpenMontage HyperFrames 动画规则解读:Reactive Displacement——以单一“进入弹簧”驱动因果碰撞位移的场景过渡
【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage
本文以 OpenMontage 仓库中
.agents/skills/hyperframes-animation技能体系下的原子动画规则 reactive-displacement.md 为骨架,完整还原其 HTML / CSS / GSAP 配方与全部调参指南,并借助 HyperFrames 组合契约、相邻动画规则与真实蓝图示例做纵深展开。读完你将能:独立实现“新内容把旧内容物理撞出画面”的因果型转场,掌握单一驱动源(single source of truth)的时间线写法,并为每个关键参数(DRIVER_DUR、BOUNCE_FACTOR、VICTIM_FRACTION、STAGE_W、DWELL_MIN等)建立可复用的取值直觉。
1. 规则定位:在 HyperFrames 动画体系里它扮演什么角色
OpenMontage 将视频渲染能力沉淀为一系列Agent 可消费的技能(skill)。其中.agents/skills/hyperframes(能力总览)指向三个领域技能:.agents/skills/hyperframes-animation(动画知识)、.agents/skills/hyperframes-core(组合契约)、.agents/skills/hyperframes-cli(命令行)。动画技能内部又把知识分成五类:rules(原子动作配方)、blueprints(多拍场景模板)、transitions(场景间转场)、techniques(通用技法)、adapters(各运行时的 API),详见 SKILL.md。
reactive-displacement就是 rules 目录中的一颗“原子”,在 rules-index.md 中被归入Transition & Motion类别,标签为transition, physics, collision, displacement, push。它的推荐用法是:每个场景挑选 2–4 条规则,用同一条 paused GSAP timeline 粘合成一场编排,而不是从头编写大段动画逻辑。
与它容易混淆的另外两条转场规则,需要先厘清边界:
| 规则 | 核心手段 | 因果性 |
|---|---|---|
| reactive-displacement | 进入元素的弹簧派生退出元素的位移 | 是因果的:A 移动是因为 B 撞上了它 |
| scale-swap-transition | 同步 shrink-out + spring pop-in,模拟“同形状状态互换” | 时间上重叠,但不构成因果 |
| card-morph-anchor | 单一容器在尺寸 / 圆角 / 表面质感之间 morph,直接充当转场锚点 | 容器自变,无“撞击”语义 |
当你的镜头语言需要表达“某物带着动量登场,并把旧内容物理地撞出画面”时,Reactive Displacement 是唯一语义正确的答案;若只是想温和地换内容,应改用 scale-swap-transition 或 card-morph-anchor。
2. 工作原理:一个驱动器,两个派生位移
规则的核心思想只有一句:退出动画是被“数学上派生”出来的,而不是独立编写的。全片只有一个0→1的驱动 tween——即“进入弹簧”(entry spring),它同时喂养两条派生运动:
- Intruder(闯入者 B,进入):位置从画外插值到居中落定;
- Victim(受害者 A,退出):位置沿相反方向从居中插值到画外,但它只在驱动值的
VICTIM_FRACTION比例处就已完成退出(不是 1.0)。
正因为受害者的退出先于闯入者的进入结束,才产生了“hit then settle(先撞击、后落定)”的节奏:撞击发生的那个瞬间就是视觉重音,而当闯入者最终滑到画面中央时,受害者早已消失——观众看到的不是两段相邻动画,而是一次“被撞飞”。
由于两条运动共享同一个经过 ease 处理的驱动值,撞击时刻在数学上是严格同步的。这与“给 intruder 和 victim 各写一条独立 tween”有本质区别:后者只是碰巧在时间上靠近,观众会读出“两个东西各自动了一下”,而不是“撞了一下”。
与 HyperFrames 组合契约的衔接
从.agents/skills/hyperframes-core的 SKILL.md 可知,每个 HyperFrames 组合要求:恰好一条gsap.timeline({ paused: true }),注册在window.__timelines["<id>"](key = 根节点data-composition-id),渲染时长由根节点data-duration决定而非时间线长度;并且整个时间线必须同步构建于页面加载期,禁止在async/setTimeout/Promise内构造。这条规则给出的 GSAP 代码完全遵循上述契约:tl以paused: true创建,末尾以window.__timelines["collide-scene"] = tl;注册,且没有tl.play()、没有Math.random、没有repeat: -1。
3. HTML:两个同轴卡片 + 场景根
完整的场景根节点必须携带 HyperFrames 的计时属性,其中id与data-composition-id均取collide-scene(注册 key 与根 id 三处一致,见 hyperframes-core 的 host-id 规则)。
<div class="scene" id="collide-scene" >.scene { position: relative; width: 100%; height: 100%; overflow: hidden; background: radial-gradient(ellipse at center, {bgColor} 0%, {bgColorDeep} 70%); font-family: {font}; } .stage { position: absolute; inset: 0; display: grid; place-items: center; } .card { position: absolute; /* both at center; transform translates them */ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px; padding: 64px 80px; border-radius: 28px; will-change: transform, opacity; } .victim { background: linear-gradient(160deg, {victimTint} 0%, {bgColorDeep} 70%); border: 1px solid {victimTint}; z-index: 1; } .intruder { background: linear-gradient(160deg, {intruderTint} 0%, {bgColorDeep} 70%); border: 2px solid {intruderBorder}; box-shadow: 0 28px 96px {intruderTint}; z-index: 2; } .card-title { font-size: 200px; font-weight: 900; color: {textColor}; line-height: 1; letter-spacing: -4px; } .card-sub { font-size: 36px; font-weight: 800; letter-spacing: 10px; text-transform: uppercase; color: {accentColor}; text-align: center; }CSS 层需要特别解释的四个设计决策:
.scene { overflow: hidden; }是硬性约束。两张卡都会飞出画面,若不禁用滚动裁剪,退场与进场元素会把页面撑出横向滚动条,甚至破坏取景。z-index明确写死:.intruder(2)在.victim(1)之上。重叠瞬间闯入者必须“盖”在受害者前方——它是碰撞的赢家;否则受害者会显得从闯入者身体里“穿模”而过。will-change: transform, opacity同时声明在两卡上:位移与淡出都由 transform / opacity 承载,这既规避了 HyperFrames core 禁止对display/visibility做动画的约束,也让合成器可以提前分层优化。- 两张卡都
position: absolute并靠place-items: center对齐,随后用translate(-50%, -50%)让元素中心与舞台中心重合——后续所有位移都发生在“居中静止”这一基准之上,x: 0即视觉正中。
颜色 token({bgColor}、{bgColorDeep}、{victimTint}、{intruderTint}、{intruderBorder}、{textColor}、{accentColor}、{font})应由组合的样式令牌解析后替换,保持与 hyperframes-creative 的主题一致性(对比度与可读性属于该技能的范围)。
5. GSAP Timeline:单驱动 + onUpdate 派生两卡
5.1 完整脚本
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script> <script> window.__timelines = window.__timelines || {}; const tl = gsap.timeline({ paused: true }); // Off-stage distances are derived from the stage width. const INTRUDER_START_X = STAGE_W; // off-stage right const VICTIM_END_X = -STAGE_W; // off-stage left, opposite direction // Initial state — victim centered, intruder off-stage right gsap.set("#victim", { x: 0, opacity: 1, rotation: 0 }); gsap.set("#intruder", { x: INTRUDER_START_X, opacity: 0, rotation: -INTRUDER_TILT }); // Single driver — the entry spring — runs 0→1 over the impact arc const driver = { p: 0 }; tl.to( driver, { p: 1, duration: DRIVER_DUR, ease: `back.out(${BOUNCE_FACTOR})`, // intruder spring onUpdate: () => { // Intruder: full 0→1 progress maps to enter (off-stage → center) const intruderX = INTRUDER_START_X * (1 - driver.p); const intruderOpacity = Math.min(1, driver.p * FADE_IN_SHARPNESS); const intruderRot = -INTRUDER_TILT * (1 - driver.p); // settle to 0° const intruder = document.getElementById("intruder"); intruder.style.transform = `translate(-50%, -50%) translateX(${intruderX}px) rotate(${intruderRot}deg)`; intruder.style.opacity = String(intruderOpacity); // Victim: completes exit at VICTIM_FRACTION of driver (intruder still flying in) // so the impact MOMENT is the visual punch — by the time intruder centers, // victim is already off-stage. const victimP = Math.min(1, driver.p / VICTIM_FRACTION); const victimX = VICTIM_END_X * victimP; const victimOpacity = 1 - victimP; const victim = document.getElementById("victim"); victim.style.transform = `translate(-50%, -50%) translateX(${victimX}px)`; victim.style.opacity = String(victimOpacity); }, }, DRIVER_AT, ); // Climax dwell — intruder holds at center after settle (no additional motion; // composition continues with intruder centered for ≥ DWELL_MIN seconds). window.__timelines["collide-scene"] = tl; </script>5.2 逐段拆解
初始化(gsap.set):victim 在x:0, opacity:1, rotation:0(居中静止),intruder 在+STAGE_W处、opacity:0、并带一个-INTRUDER_TILT的初始倾角。值得注意的是 intruder 初始就带倾角、落定时归 0°——这是“带着旋转砸进来再扶正”这一动量可视化叙事的伏笔。
驱动对象(const driver = { p: 0 }):这是全片唯一真正被 tween 的对象。p从 0 走到 1,时长DRIVER_DUR,缓动为back.out(${BOUNCE_FACTOR})——即“进入弹簧”本体。这个onUpdate是唯一允许读取进度的位置,不要再用第二条tl.to()去独立补间 intruder 或 victim。
Intruder 派生公式:
intruderX = STAGE_W * (1 - p):p=0时在 +STAGE_W(画外右侧),p=1时回到 0(居中);intruderOpacity = min(1, p * FADE_IN_SHARPNESS):FADE_IN_SHARPNESS越大,越早在运动中“显影”为实体;intruderRot = -INTRUDER_TILT * (1 - p):随p→1倾角衰减为 0°;- transform 写法
translate(-50%, -50%) translateX(...) rotate(...)必须保留居中对齐基准,再叠加业务位移/旋转。
Victim 派生公式:
victimP = min(1, p / VICTIM_FRACTION):把驱动进度“放大”映射——当p只走到VICTIM_FRACTION(典型 0.5)时,victimP已经到达 1,即 victim 完成整个退场;victimX = -STAGE_W * victimP:向负 X(左侧画外)滑出——与 intruder 进入方向严格反向同轴;victimOpacity = 1 - victimP:位移与淡出同步,线性滑出并淡出。
注释里的“撞击瞬间”:规则文档刻意强调——当 intruder 仍在飞入、尚未居中时 victim 已经离场。两卡“重叠”的那一帧才是撞击,此后 victim 只是在撤出 intruder 即将占据的空间。若 victim 拖到p=1才走完,两段运动会读成“平行运动”而非“因果撞击”。
Climax dwell(高潮停留):驱动结束后脚本不再追加任何运动,画面停留在 intruder 居中的静止帧至少DWELL_MIN秒——这是新内容被观众“阅读”的窗口,规则明确要求不许跳过。
关于确定性:动画技能要求预先计算布局常量、禁止在 tween 时调用
getBoundingClientRect()(渲染器是并行采样的,tween 期测量会导致失步)。上面的STAGE_W、INTRUDER_START_X都是组合 setup 期算好的常量,符合该约束。可参考 animation-map.mjs 在编排后对window.__timelines做盘点审计。
6. 参数调优指南:何时该用哪个值
6.1 参数速查总表
| 参数 | 语义 | 建议范围 | 一句话经验 |
|---|---|---|---|
DRIVER_AT | 进入弹簧的起始时刻 | 依相(phase)而定,通常开画数秒后 | 太早吃掉 setup 拍子,太晚卡顿剪接点 |
DRIVER_DUR | 闯入者完整进入时长 | 0.6–1.4 s | 短=干脆重拳;长=沉重落地 |
BOUNCE_FACTOR | back.out()回弹系数 | 1.2–2.0(典型 1.4–1.6) | 低=紧实落定;高=过冲弹跳 |
VICTIM_FRACTION | victim 在驱动进度中的完成比例 | 0.4–0.5(上限 ~0.6) | 低于 0.4 撞击感读不出;高于 0.6 变成平行运动 |
STAGE_W | 舞台宽度(px),用于摆放画外元素 | 等于组合渲染宽度(data-width) | 过小会让画外元素露边 |
INTRUDER_TILT | 闯入者初始倾角(deg) | 5–15°(典型 ~10°) | 低=干净滑入;高=可见的“旋入扶正” |
FADE_IN_SHARPNESS | intruder 显影速度乘数 | 3–8(1/FADE_IN_SHARPNESS处即不透明) | 高=早显影、读作实体 |
DWELL_MIN | 落定后的高潮停留 | ≥ 1.0 s(典型 1.0–1.5 s) | 这是新内容被阅读的窗口 |
6.2 逐参数详解
DRIVER_AT—— 什么时候开始撞击
- 相位相关,通常在组合开画后数秒,示例会安排在“前一段旁白/阅读拍子落定之后”再触发位移;
- 太早:观众还没读完 setup 拍,撞击抢戏;太晚:画面停滞、转场拖沓;
- 硬约束:必须保证在组合结束前留出 ≥
DWELL_MIN的高潮停留。
DRIVER_DUR—— 撞击有多快
- 0.6–1.4 s;短时长读作 zippy/punchy(敏捷重拳),长时长读作 heavy/landed(沉重落地);
- 约束:需与
BOUNCE_FACTOR联动——长时长配高 bounce会读作“漂浮”,应压低回弹。
BOUNCE_FACTOR—— 弹簧性格
back.out系数,范围 1.2–2.0,示例常落在 1.4–1.6;- 低值 ≈ 紧实落定,高值 ≈ 过冲/弹跳;
- 约束:ease 家族保持
back.out;若想引入振荡才升级为elastic.out——换家族等于重写手感。
VICTIM_FRACTION—— 因果性的关键旋钮
- 0.4–0.5,硬上限约 0.6;
- 低于 0.4:victim 在撞击还没被看清时就消失了,观众不知道“它为什么走”;高于 0.5–0.6:victim 滑出的节奏拖到与 intruder 同步,碰撞隐喻失效、读作两条平行动画;
- 本规则的标准形态取 ~0.5。
STAGE_W—— 画外的距离基准
- 应等于组合渲染宽度(即根节点声明的
data-width),使元素平移一个完整画宽后完全出画; - 取值过小会导致开场时 intruder 部分可见(露边穿帮)。
INTRUDER_TILT—— 旋入的可见度
- 5–15°,典型中量感冲击约 10°;
- 符号必须与进入方向一致(同轴冲量传递的直觉),方向反了会读作“被弹回来”。
FADE_IN_SHARPNESS—— 显影速度
- 3–8(要求 >1);intruder 在
1/FADE_IN_SHARPNESS处进度即达到不透明; - 低值:伴随运动的柔和淡入;高值:很早显影、读作实体高速砸入。
DWELL_MIN—— 高潮后的阅读窗口
- ≥1.0 s,典型 1.0–1.5 s;
- 太短显得仓促、内容来不及读;过长则拖住整个组合;
- 规则文档特别强调:“撞击后的停留是新内容被阅读的地方——不要跳过”。
7. 变体:四种可叠加的“撞击后处理”
7.1 受害者受击旋转(Impact rotation on victim)
victim 不只是滑出去,还带着受击角旋转着飞出。只需把 transform 叠加一个随victimP增长的旋转项:
const victimRot = victimP * -VICTIM_KICK_DEG; // rotates as it slides victim.style.transform = `translate(-50%, -50%) translateX(${victimX}px) rotate(${victimRot}deg)`;VICTIM_KICK_DEG通常取 15–25°,幅度应与观众感知到的 intruder“质量”匹配——撞得越重、kick 越大。
7.2 纵向碰撞(Vertical collision)
intruder 从上方砸入、victim 被压向下方——同样的数学换成 Y 轴即可。视觉语义从“被撞飞”变成“被重物压落”。
7.3 落定后的衰减摆动(Wobble after settle)
intruder 居中后先做一次阻尼正弦摆动再静止,营造“撞击余震”,然后才进入高潮停留。实现上在DRIVER_AT + DRIVER_DUR处再驱动一个相位对象:
const wobble = { p: 0 }; tl.to( wobble, { p: Math.PI * WOBBLE_CYCLES * 2, duration: WOBBLE_DUR, ease: "none", onUpdate: () => { const rot = Math.sin(wobble.p) * WOBBLE_AMP_DEG * (1 - wobble.p / (Math.PI * WOBBLE_CYCLES * 2)); // linear decay intruder.style.transform = `translate(-50%, -50%) rotate(${rot}deg)`; }, }, DRIVER_AT + DRIVER_DUR, );注意(1 - p/(2π·CYCLES))这一项是线性衰减包络——幅度随摆动相位收敛到 0,保证摆完后是绝对静止(衔接高潮停留时无跳变)。摆动 tween 也必须走ease: "none"(正弦本身提供节奏),与 sine-wave-loop 的相位驱动手法同源。
7.4 多受害者级联(Multi-victim ripple)
intruder 依次撞飞一排对齐的卡片,每个 victim 拿到不同的驱动相位偏移、先后滑出,形成级联涟漪。实现上为每个 victim 复制一条“派生式计算”,只是各自的victimP使用(driver.p - offset_i)裁剪到[0,1]后除以各自的 fraction。
8. 核心原则:让“撞击”看起来是真的
- 单驱动 = 单一事实来源:进入弹簧同时驱动双方运动。为 intruder 与 victim 分别写独立 tween 会摧毁因果链——它们只是碰巧在时间上靠近,而不是相撞。
- Victim 在驱动的一部分进度处完成退出:intruder 抵达中心时 victim 已彻底消失。“撞击”是二者重叠的那一瞬;此后 victim 只是在撤出 intruder 将要占据的空间。
- 有方向的动量传递:intruder 从正 X 来 → victim 往负 X 走,严格同轴。若两者走不同轴,会读成“擦肩而过”而非“相撞”。
- Intruder 的 z-index 高于 victim:重叠期闯入者必须在前面(它是碰撞的赢家),否则 victim 看起来“穿隧”而过。
- Intruder 带旋转进入、落定扶平:小倾角 → 0° 的收敛,读作“旋入然后落地生根”(spin-and-plant),是动量的可视化补充。
- 撞击后必须有高潮停留:撞击是头条节拍(headline beat),撞击后的停留是新内容被阅读的地方。
9. 硬性约束:与 HyperFrames 契约的对齐清单
规则正文列出的 Critical Constraints,可与 hyperframes-core 的 Non-Negotiable Rules 互相印证:
- 时间线必须 paused:
gsap.timeline({ paused: true }),绝不tl.play();渲染器靠 seek 逐帧推进。 - 注册 key =
data-composition-id:window.__timelines["collide-scene"]必须与场景根 id /data-composition-id三方一致(hyperframes-core 称之为 host-id 规则)。 - 单驱动、多派生值集中于同一个
onUpdate:禁止用分离的tl.to()分别补间 intruder 和 victim。 .scene必须overflow: hidden:画外运动会超出帧。- 两卡都要
will-change: transform, opacity:仅动画可视属性白名单内的属性。 - Intruder z-index 显式 > victim:不依赖 DOM 顺序,明确书写。
- 布局常量预计算(动画技能叠加约束):
STAGE_W等常量在组合 setup 期确定,tween 期不做 DOM 测量、不用Math.random/Date.now/performance.now、不用repeat: -1、不用 CSS@keyframes驱动主时间线。 - 渲染时长以
data-duration为准:本示例data-duration="3",需保证DRIVER_AT + DRIVER_DUR + DWELL_MIN ≤ 3,否则高潮停留会被切掉。
以上结构性错误通常无法被lint/validate/inspect自动发现(见 hyperframes-core 的“silent bugs”清单),所以写完后应执行:npx hyperframes lint(0 错误)→npx hyperframes validate→npx hyperframes inspect→ 必要时用npx hyperframes snapshot --at <midpoints>逐帧目检 → 用户批准后npx hyperframes render。
10. 组合玩法:如何与其它规则拼出完整镜头
规则的 Combinations 章节给出了三条典型的“拼装”路径:
- hacker-flip-3d:intruder 的文本在进入阶段用字符级 3D 翻转(解密效果)揭示——闯入者边砸边“解码”,强化数据冲击叙事;
- sine-wave-loop:intruder 落定进入高潮停留后,叠一层有界低频呼吸/抖动,防止静止帧“死掉”。注意该规则的礼仪:优先用 VO 驱动的顺序揭示填满镜头,jitter 其次,呼吸是最后手段,且保持低振幅;
- vertical-spring-ticker:把 intruder 换成一台“把旧内容顶出去”的纵向弹簧 ticker——slot-machine 式的逐格滚动与位移撞击结合。
蓝图级应用:ticker-takeover
这类“规则拼接”在.agents/skills/hyperframes-animation/blueprints的成品蓝图中是真实落地的。以 ticker-takeover.md 为例,它描述了一个Ticker 位移 / 接管场景:
- Scene 1(0.0–1.4s):typewriter 逐字铺出引导短语;
- Scene 2(1.4–3.0s):一行中的强调词像老虎机一样纵向滚过 2–3 个候选词(
vertical-spring-ticker); - Scene 3(3.0–4.2s,签名动作):hero 从画外带动量砸入并把整组文本物理地撞出画面——蓝图明确标注这条运动映射到
reactive-displacement(文本是被位移的质量体),并给出调参建议:把 hero 的“重落地”表达为更长的power2settle,而不是规则默认的back.out; - Scene 4(4.2s–结束):hero 居中静止阅读,叠加低振幅 sine-wave-loop。
这恰好展示了 Reactive Displacement 在“物理接管”叙事中的经典用途,也说明参数默认值不是教条——当闯入者是“重物”时应按语义改写 settle 缓动。
11. 实战落地清单与避坑提示
编写一个可渲染的 Reactive Displacement 组合,推荐按下述顺序走查:
- 确认舞台尺寸:
STAGE_W取组合渲染宽度,确保 intruder 初始完全在画外。 - 确定拍序:先让上一内容/文本阅读拍落定(
DRIVER_AT在它之后),再定 intruder 进入节奏与落地留白。 - 先锁 causal 骨架:以
DRIVER_DUR、VICTIM_FRACTION≈0.5、BOUNCE_FACTOR≈1.5起步,先让“撞击”成立,再调性格。 - 调性格顺序:手感短/重由
DRIVER_DUR决定;软/弹由BOUNCE_FACTOR决定;撞击清晰度由VICTIM_FRACTION决定;画外进出量由STAGE_W决定;动感由INTRUDER_TILT/ 变体旋转决定;实体感由FADE_IN_SHARPNESS决定。 - 确认高潮停留:
DRIVER_AT + DRIVER_DUR + DWELL_MIN ≤ contenteditable="false">【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考