首页
/ HyperFrames 两阶段相机光标跟踪:为 OpenMontage 打造打字驱动的确定性虚拟镜头

HyperFrames 两阶段相机光标跟踪:为 OpenMontage 打造打字驱动的确定性虚拟镜头

2026-09-07 18:47:38作者:庞队千Virginia

导读

本文讲解 OpenMontage 仓库中 HyperFrames 动画技能库的原子规则 camera-cursor-tracking完整规则原文)。当一条水平增长的文案(不断键入的搜索框、渐次出现的超长 URL)需要始终完整地呈现在镜头内时,这条规则通过"静态锚定 → 追踪跟随"的两阶段虚拟相机,把视口锁在一个持续移动的焦点(光标、高亮块、最后一个键入字形)上。读完后,你将掌握:两阶段偏移量的数学表达与无缝切换、可在多 worker 并行渲染下保持确定性的 GSAP 时间轴写法、全套可调参数(从光标几何到相机缓动)的取值策略,以及如何将这条规则与同仓库的上下文光标、离散文本序列等规则组合成多阶段镜头。

HyperFrames 是 OpenMontage 内置的"用 HTML 渲染视频"技能体系——一条可渲染合成(composition)就是一份用 data-* 属性声明时序的 HTML 文件(见 hyperframes 入口技能)。camera-cursor-tracking 属于 hyperframes-animation 下的原子运动规则(rules),与多阶段相机、坐标定点缩放等规则并列,可被诸如 cursor-ui-demo 蓝图 这样的多阶段场景模板引用为"主相机"。

它是如何工作的:两阶段虚拟相机

核心思路是把两个"空间"彻底分离:

  • World Space(世界空间)——完整的目标元素,包含全部内容;
  • Screen Space(屏幕空间)——视口(viewport),观众实际看到的窗口。

相机在两个阶段之间切换:

  • Phase 1(静态):世界容器停在固定的初始偏移 INITIAL_OFFSET 上,相机不动。这样在追踪开始前,观众视线已被锚定在当前构图,镜头不会"抢戏"。
  • Phase 2(追踪):当焦点(光标、高亮、最后键入的字形)越过目标屏幕位置——例如可配置的比例 CURSOR_TARGET_FRACTION × viewportWidth——世界容器向左平移(x: -<delta>),把焦点始终"钉"在那个屏幕位置上。

相位边界处的连续数学

两阶段的交接在数学上是连续的——追踪启动的瞬间,世界位置恰好等于静态阶段该有的值,因此过渡无跳变。代码中使用的分段形式是:

finalWorldX = Math.min(INITIAL_OFFSET, trackingOffset)

其中 INITIAL_OFFSET 是静态阶段的值;trackingOffset 是让焦点保持在 CURSOR_TARGET_FRACTION × viewportWidth 所需的任何位移。只要焦点还没有长过目标屏幕 X,trackingOffset 就会大于 INITIAL_OFFSET(它是一个"不那么负"的数),于是 Math.min 返回静态值;一旦焦点越过目标,trackingOffset 反超 INITIAL_OFFSET,追踪接管。

强调:这条规则的作者明确警告不要改成 if (typingProgress > threshold) 的硬分支——硬分支会在相位边界造成可见的镜头跳动(见下文"关键约束")。

合成(Composition)骨架:HTML 与 data-* 属性

场景根元素声明了整个合成的时序元数据。data-composition-id 是关键——它必须与动画注册表键(window.__timelines["tracking-scene"])一致;data-duration 是渲染时长而非时间轴长度(data-attributes 参考)。

<div
  class="scene"
  id="tracking-scene"
  data-composition-id="tracking-scene"
  data-start="0"
  data-duration="5"
  data-track-index="0"
>
  <div class="viewport">
    <div class="world">
      <div class="search-bar">
        <span class="text" id="reveal-text">{phrase}</span><span class="cursor">|</span>
      </div>
    </div>
  </div>
</div>

注意结构层级:scene(合成根)→ viewport(视口,负责裁剪)→ world(可平移的世界容器)→ search-bartext(可增长文本)与 cursor(内联跟随的光标块)。在整个 HyperFrames 生态中,可见的计时元素通常还需要 class="clip" 标记,否则运行时会让它全程保持可见而忽略 data-start/data-duration(本规则示例以动画驱动文本显现,故用时间轴 maxWidth 揭示而非 clip 窗口控制)。

布局:CSS(hero-frame 布局)

CSS 的核心分工是:.viewportoverflow: hidden 裁剪出"镜头",.world 保持 white-space: nowrap 让文案单行排布以配合相机数学,transform: translateX(0) 预留出 GSAP 的动画空间。

.scene {
  position: relative;
  width: 100%;
  height: 100%;
}

.viewport {
  position: absolute;
  inset: 0;
  overflow: hidden; /* clip the world content */
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding-left: VIEWPORT_PAD_LEFT; /* "left margin" — variation: left-aligned init */
}

.world {
  display: flex;
  align-items: center;
  white-space: nowrap; /* keep text on one line for camera-tracking */
  transform: translateX(0); /* GSAP will animate this */
}

.search-bar {
  font-family: {font};
  font-size: BAR_FONT_SIZE;
  font-weight: BAR_FONT_WEIGHT;
  color: {textColor};
  letter-spacing: BAR_LETTER_SPACING;
}

.search-bar .text {
  /* Width grows as more characters reveal */
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}

.search-bar .cursor {
  display: inline-block;
  width: CURSOR_WIDTH;
  margin-left: CURSOR_GAP;
  background: {accentColor};
  height: CURSOR_HEIGHT_EM;
  vertical-align: bottom;
  /* No `animation: blink` CSS keyframe here — HF renders by seeking a paused
     timeline, and CSS animation clocks are NOT synced to that seek. A CSS
     blink will flicker non-deterministically. Drive cursor blink as a finite
     yoyo tween on the GSAP timeline instead — see GSAP Timeline section. */
}

两个要点:

  1. CSS 注释本身就是约束文档——.cursor 上禁止 CSS @keyframes 闪烁动画,因为 HyperFrames 通过"寻找已暂停时间轴"的方式渲染每一帧,CSS 动画时钟与该 seek 不同步,必然闪屏。闪烁必须由 GSAP 时间轴上的有限 yoyo tween 驱动。
  2. VIEWPORT_PAD_LEFT 必须与 .viewportpadding-left 严格一致,否则相机数学会漂移(见下文参数表)。

驱动:GSAP 时间轴

整条规则围绕一条暂停的 GSAP 时间轴展开,且必须在脚本解析期同步构建

<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 });

  // Pre-measure target text width to compute tracking distance.
  // Build the timeline SYNCHRONOUSLY — see Critical Constraints for why
  // a fonts.ready gate causes worker-race flicker.
  const textEl = document.getElementById("reveal-text");
  const fullText = textEl.textContent;
  const targetCursorScreenX = CURSOR_TARGET_FRACTION * VIEWPORT_WIDTH;
  const fullWidth = textEl.scrollWidth; // total text width after full reveal
  const trackingDelta = Math.max(0, VIEWPORT_PAD_LEFT + fullWidth - targetCursorScreenX);

  // Phase 1 — text reveals progressively; camera holds.
  // Reveal via max-width tween (no layout-property tweens on width/left/top).
  tl.fromTo(
    ".search-bar .text",
    { maxWidth: 0 },
    {
      maxWidth: fullWidth,
      duration: REVEAL_DUR,
      ease: "none", // linear typing rate
    },
    REVEAL_START,
  );

  // Phase 2 — camera tracks. Begin BEFORE full reveal so the boundary feels
  // continuous (text is still revealing as camera starts moving). The
  // Math.min(INITIAL_OFFSET, trackingOffset) formulation makes the handoff
  // mathematically continuous; see How It Works.
  tl.to(
    ".world",
    {
      x: -trackingDelta,
      duration: TRACK_DUR,
      ease: "power2.inOut",
    },
    TRACK_START,
  );

  // Cursor blink — GSAP-driven (NEVER CSS @keyframes infinite, which doesn't
  // sync with HF's seek-by-frame). Finite yoyo, repeats computed from scene
  // length so blinks land deterministically across frames.
  const blinkRepeats = Math.ceil(SCENE_DURATION / BLINK_HALF_PERIOD) - 1;
  tl.to(
    ".search-bar .cursor",
    {
      opacity: 0,
      duration: BLINK_HALF_PERIOD,
      ease: "steps(1)", // hard on/off, no fade
      yoyo: true,
      repeat: blinkRepeats,
    },
    0,
  );

  window.__timelines["tracking-scene"] = tl;
</script>

时间轴的三段编排解读

  1. 文本揭示(Phase 1)fromTo.textmaxWidth 从 0 动画到 fullWidth。选择 maxWidth 而非 width 是有意为之——这是动画属性白名单内的布局安全做法;ease: "none" 保证每字符击键节奏线性,任何缓动都会扭曲打字手感。文本宽度用 textEl.scrollWidth 预测量,而不是"字符数 × 字号"。
  2. 相机追踪(Phase 2)tl.to(".world", { x: -trackingDelta, ...})TRACK_START 位置插入。位移量在开头一次性算出:trackingDelta = Math.max(0, VIEWPORT_PAD_LEFT + fullWidth - targetCursorScreenX),即"文本总宽度超出目标光标屏位的那一部分"。追踪刻意在揭示完成之前开始(TRACK_START < REVEAL_START + REVEAL_DUR),让相位切换像一次自然的跟拍。
  3. 光标闪烁:有限 yoyo tween。repeat 次数由场景长度推导:Math.ceil(SCENE_DURATION / BLINK_HALF_PERIOD) - 1,保证闪烁能确定性地覆盖整个场景长度、不跨帧错乱;ease: "steps(1)" 提供硬开关无淡变。

这段时间轴完整符合 HyperFrames 的动画运行时契约(determinism-rules):暂停时间轴、注册键与 data-composition-id 一致、绝不在 async/Promise/setTimeout 中构建、使用有限 repeat 而非 repeat: -1

变体(Variations)

三种已验证的配置模式覆盖了最常见的取景需求:

  • 居中 → 中心追踪(Centered → Center-Tracked):设置 .viewport { justify-content: center; padding: 0; }。一旦焦点越过中线(CURSOR_TARGET_FRACTION = 0.5)相机即开始追踪。
  • 左对齐 → 右追踪(Left-Aligned → Right-Tracked):即上文完整示例。最适合内容一开始就超过视口宽度的场景(如整段 URL 渐现)。
  • 连续打字驱动器(Continuous typing driver):用 onUpdate 打字时钟(charsTyped = Math.floor(progress))替代 maxWidth tween,并在每一帧用 measureNodeWidth 驱动光标屏幕 X。当键入文本在场景别处被消费时(例如被父级条带的相机偏移量读取)必须使用此变体。

参数选择指南

规则附带了完整的参数取值表,逐条给出取值范围、效果与约束。下面是整理后的速查表,可作为落地时的默认参考:

参数 范围 / 取值 效果与约束
VIEWPORT_PAD_LEFT 0 → 约视口宽度的 10% Phase 1 锚定 X。0 贴左边缘;越大越像居中的 hero 取景。必须与 .viewportpadding-left 一致,否则相机数学漂移
VIEWPORT_WIDTH 合成的 data-width(CSS 像素) 必须等于场景根的 data-width永不被 tween
CURSOR_TARGET_FRACTION 0.5(中心追踪)→ 0.75(右倾,光标后有更多已揭示文本) 取值越低,画面内已揭示文本越少;越高则追踪启动越晚
BAR_FONT_SIZE 约为视口高度的 8%–12% 低于约 6% 时读感会从"电影化元素"降级为"UI 控件"
BAR_FONT_WEIGHT 离散;400(中性演示文本)/ 700(hero/标题取景) 按叙事层级选择
BAR_LETTER_SPACING 轻微负值(更紧致、更电影化)→ 0(默认) 条内字距微调
CURSOR_WIDTH 约 4–10 px(1080p) 更细读作"输入光标";更粗读作块状插入符
CURSOR_GAP 数 px 呼吸空间 不要超过光标宽度,否则视觉上会脱开
CURSOR_HEIGHT_EM 0.85–1.0 匹配所打字形的视觉高度
REVEAL_START 通常为 0 若前面还有其他相位,需 ≥ 该相位结束 + 小缓冲
REVEAL_DUR 随字符数缩放,单字符节奏约 0.05–0.15 s 必须早于 SCENE_DURATION 结束,最好与追踪相位略有重叠
TRACK_START 通常早于揭示完成;若焦点在 t=0 已越过目标,可等于 REVEAL_START TRACK_START < REVEAL_START + REVEAL_DUR 保证平滑交接
TRACK_DUR 0.8–2.0 s 低于 0.5 s 读作跳动(snap);超过 2.5 s 拖沓
SCENE_DURATION 必须等于场景根的 data-duration 喂给闪烁 repeat 计数;不匹配会导致闪烁提前截断或越界
BLINK_HALF_PERIOD 0.2–0.4 s(0.3 s 最接近自然光标闪烁) 派生值 Math.ceil(SCENE_DURATION / BLINK_HALF_PERIOD) - 1 必须 ≥ 0
缓动选择 离散 相机平移用 power2.inOut/power3.inOut 营造电影化落定;避免 back.out(过冲像 UI 弹跳而非相机)。揭示用 "none" 线性;闪烁用 "steps(1)" 硬开关

关键原则

  • getBoundingClientRect() / 探针节点测量宽度,不要用"字符数 × font-size"推算——比例字体各字形宽度不同。
  • 世界容器 .world 必须 white-space: nowrap——文本必须保持单行,相机数学才成立。
  • 预分配世界宽度:把 maxWidth 设为满目标宽度,避免 tween 中途出现布局位移。
  • 相机用缓动而非直线运动power2.inOut/power3.inOut),才像自然运镜。
  • 用缓动近似弹簧感:GSAP 没有内建弹簧,但 back.out(${BOUNCE_FACTOR})power4.out 可以近似"落定回弹"的手感。

关键约束(Critical Constraints)

这部分是全规则最容易踩坑的地方,值得逐条展开:

  1. 时间轴必须同步构建、禁止 fonts.ready 门控。HyperFrames 在并行 worker 中渲染各帧,每个 worker 都是全新浏览器。如果把时间轴构建包在 document.fonts.ready.then(...) 里,部分 worker 会在 Promise 解析前就 seek 帧,找不到已注册的时间轴 → 该帧以 CSS 初始态渲染(例如 max-width: 0 导致文本为空);另一些 worker 则正常 → 空/满文本之间出现可见闪烁。应在脚本解析期就把 window.__timelines[id] = tl 注册好——即便字体尚未加载。相机数学能容忍回退字体测量造成的几个百分点的宽度误差,但 worker 竞争闪烁不可接受。
  2. 如果确实需要精确的字体加载后测量:在 tween 的 onUpdate 里重测(对逐帧 seek 仍然是确定性的),而不是用 Promise 门控;或者在 @font-face 上设 font-display: block,强制浏览器在绘制任何文本前等待字体。
  3. 时间轴必须暂停gsap.timeline({ paused: true }),永远不调用 tl.play()。这正对应 determinism-rules 中"动画状态必须可由 HyperFrames 时间 seek"的核心要求——渲染器没有"播放"概念,每帧都是对时间值的一次全新 seek。
  4. 注册表键 = data-composition-idwindow.__timelines["tracking-scene"] 必须与场景根匹配。
  5. 相位边界处数学必须连续:追踪启动瞬间世界的 x 必须等于静态阶段的值。Math.min(INITIAL_OFFSET, trackingOffset) 保证这一点;不要改成 if (typingProgress > threshold) 硬分支,否则镜头会可见跳变。
  6. 光标内联而非绝对定位:光标必须是文本的兄弟元素(inline-block),随文本流自然跟随——绝对定位会与相机数学错位。
  7. .viewport 必须 overflow: hidden:裁剪世界左边缘在平移出画时的泄漏。
  8. 光标闪烁必须由 GSAP 驱动,禁止 CSS @keyframes ... infinite:HyperFrames 靠 seek 暂停时间轴渲染,CSS 动画时钟与 seek 不同步,任何 CSS 驱动的闪烁都会跨帧不确定地闪屏。闪烁永远是暂停时间轴上的一次有限 yoyo tween,repeat 次数由场景长度计算得出。

这些约束与 hyperframes-animation SKILL 中"布局常量预计算、空间运动只用 GSAP transform 别名(x/y/scale/rotation)、布局属性(width/height/top/left)不得用于 layout 变更 tween"的动画工艺约定一脉相承——本规则用 maxWidth + x 正是对这一约定的具体落实。

组合与配对技能

这条规则设计成可与同目录其他原子规则自由组装:

  • context-sensitive-cursor.md:在打字过程中按文本片段切换光标颜色/样式——把"相机追光标"与"光标随片段变色"叠起来,就是完整的终端/搜索框叙事镜头;
  • discrete-text-sequence.md:非线性文本揭示(错别字、整段补充、停顿、思考间隙),与"连续打字 + 相机追踪"的平滑型形成互补。

技能级配对则对应 HyperFrames 的领域技能分工:

  • /hyperframes-animation —— 时间轴 + tween API(原子规则本库);
  • /hyperframes-core —— 合成接线与 data-* 属性(data-attributes.md);
  • /hyperframes-cli —— 用 hyperframes lint 校验注册表键与时长(lint-validate-inspect)。

在蓝图中的角色

cursor-ui-demo 蓝图 的引用可以印证它的典型用法:当需要一个"相机伺服到光标所触之处"的产品演示(Product_Intro / Key_Feature 变体)时,camera-cursor-tracking 被列为主相机(primary)——"这种对光标的相机伺服,正是该蓝图区别于无手势相机滚动与静态设备窗口导览的核心"。它通常与 viewport-change(横移/竖移/推拉的形式实现)、multi-phase-camera(把追逐拆成离散交互节拍)以及 coordinate-target-zoom(推近到被操作区域)一起组成完整的"chase"运镜链。

落地检查:用 CLI 验证合成

在完成 HTML/CSS/GSAP 组装后,HyperFrames 提供一套"先检查后渲染"的纪律(hyperframes-cli),对运动密集的场景尤其适用:

npx hyperframes lint                  # 静态检查:data-composition-id 缺失、同轨重叠、未注册时间轴
npx hyperframes validate              # headless Chrome 运行时检查:console 错误、网络失败、WCAG 对比度
npx hyperframes inspect --samples 15  # 时间轴布局扫描(默认 9 个采样点)
npx hyperframes snapshot --frames 10  # 输出关键帧 PNG 供肉眼核对

lint 能直接抓到本文反复强调的"注册表键与 data-composition-id 不匹配""时长不一致"这类缺陷;inspect 支持 *.motion.json sidecar 做运动意图断言(如 appearsBystaysInFrame),是"渲染 ≠ 预览"类 bug 最接近自动化的代理检查。对打字揭示这类逐帧 seek 场景,先跑一轮 lint + snapshot 再看 PNG,通常比直接渲染视频更快收敛。

小结

camera-cursor-tracking 给出了一个把"镜头语言"落实为确定性的两阶段数学模型的完整范例:静态锚定稳住视线、Math.min 让追踪接手时无缝衔接、暂停时间轴 + 同步注册保证多 worker 并行渲染下逐帧可复现。它的可迁移价值在于——任何"水平增长元素 + 移动焦点"的镜头问题(搜索框打字、URL 渐现、跑马灯焦点跟拍),都能复用这套 HTML/CSS/GSAP 骨架与参数调优方法,再通过上下文光标变色与离散文本序列等规则把单镜头扩成多段落的多阶段叙事。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388