OpenMontage HyperFrames 动画规则解读:Reactive Displacement——以单一“进入弹簧”驱动因果碰撞位移的场景过渡
2026/9/8 16:24:54 网站建设 项目流程

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_DURBOUNCE_FACTORVICTIM_FRACTIONSTAGE_WDWELL_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 代码完全遵循上述契约:tlpaused: true创建,末尾以window.__timelines["collide-scene"] = tl;注册,且没有tl.play()、没有Math.random、没有repeat: -1

3. HTML:两个同轴卡片 + 场景根

完整的场景根节点必须携带 HyperFrames 的计时属性,其中iddata-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 层需要特别解释的四个设计决策:

  1. .scene { overflow: hidden; }是硬性约束。两张卡都会飞出画面,若不禁用滚动裁剪,退场与进场元素会把页面撑出横向滚动条,甚至破坏取景。
  2. z-index明确写死:.intruder(2)在.victim(1)之上。重叠瞬间闯入者必须“盖”在受害者前方——它是碰撞的赢家;否则受害者会显得从闯入者身体里“穿模”而过。
  3. will-change: transform, opacity同时声明在两卡上:位移与淡出都由 transform / opacity 承载,这既规避了 HyperFrames core 禁止对display/visibility做动画的约束,也让合成器可以提前分层优化。
  4. 两张卡都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_WINTRUDER_START_X都是组合 setup 期算好的常量,符合该约束。可参考 animation-map.mjs 在编排后对window.__timelines做盘点审计。

6. 参数调优指南:何时该用哪个值

6.1 参数速查总表

参数语义建议范围一句话经验
DRIVER_AT进入弹簧的起始时刻依相(phase)而定,通常开画数秒后太早吃掉 setup 拍子,太晚卡顿剪接点
DRIVER_DUR闯入者完整进入时长0.6–1.4 s短=干脆重拳;长=沉重落地
BOUNCE_FACTORback.out()回弹系数1.2–2.0(典型 1.4–1.6)低=紧实落定;高=过冲弹跳
VICTIM_FRACTIONvictim 在驱动进度中的完成比例0.4–0.5(上限 ~0.6)低于 0.4 撞击感读不出;高于 0.6 变成平行运动
STAGE_W舞台宽度(px),用于摆放画外元素等于组合渲染宽度(data-width过小会让画外元素露边
INTRUDER_TILT闯入者初始倾角(deg)5–15°(典型 ~10°)低=干净滑入;高=可见的“旋入扶正”
FADE_IN_SHARPNESSintruder 显影速度乘数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 互相印证:

  • 时间线必须 pausedgsap.timeline({ paused: true }),绝不tl.play();渲染器靠 seek 逐帧推进。
  • 注册 key =data-composition-idwindow.__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 validatenpx 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 位移 / 接管场景:

  1. Scene 1(0.0–1.4s):typewriter 逐字铺出引导短语;
  2. Scene 2(1.4–3.0s):一行中的强调词像老虎机一样纵向滚过 2–3 个候选词(vertical-spring-ticker);
  3. Scene 3(3.0–4.2s,签名动作):hero 从画外带动量砸入并把整组文本物理地撞出画面——蓝图明确标注这条运动映射到reactive-displacement(文本是被位移的质量体),并给出调参建议:把 hero 的“重落地”表达为更长的power2settle,而不是规则默认的back.out
  4. Scene 4(4.2s–结束):hero 居中静止阅读,叠加低振幅 sine-wave-loop。

这恰好展示了 Reactive Displacement 在“物理接管”叙事中的经典用途,也说明参数默认值不是教条——当闯入者是“重物”时应按语义改写 settle 缓动。

11. 实战落地清单与避坑提示

编写一个可渲染的 Reactive Displacement 组合,推荐按下述顺序走查:

  1. 确认舞台尺寸STAGE_W取组合渲染宽度,确保 intruder 初始完全在画外。
  2. 确定拍序:先让上一内容/文本阅读拍落定(DRIVER_AT在它之后),再定 intruder 进入节奏与落地留白。
  3. 先锁 causal 骨架:以DRIVER_DURVICTIM_FRACTION≈0.5BOUNCE_FACTOR≈1.5起步,先让“撞击”成立,再调性格。
  4. 调性格顺序:手感短/重由DRIVER_DUR决定;软/弹由BOUNCE_FACTOR决定;撞击清晰度由VICTIM_FRACTION决定;画外进出量由STAGE_W决定;动感由INTRUDER_TILT/ 变体旋转决定;实体感由FADE_IN_SHARPNESS决定。
  5. 确认高潮停留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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询