首页
/ HyperFrames 场景转场实战:用 @hyperframes/shader-transitions 在 GSAP 时间线上接入 GPU 着色器过渡

HyperFrames 场景转场实战:用 @hyperframes/shader-transitions 在 GSAP 时间线上接入 GPU 着色器过渡

2026-09-08 15:26:36作者:凌朦慧Richard

HyperFrames 遵循"Write HTML. Render video."的理念:页面即时间线,场景(scene)即 DOM。@hyperframes/shader-transitions 正是为这种架构补齐最后一环的独立子包——它用 WebGL 片元着色器(fragment shader)在相邻场景之间渲染 GPU 加速转场,并通过捕获场景动画采样帧与 GSAP 时间线结合驱动。阅读本文后,你将掌握在 HyperFrames 组合(composition)中安装、配置与扩展着色器转场,理解"预捕获 + 着色器合成"的浏览器预览管线、引擎确定性渲染管线的差异,以及降级与缓存等工程细节。

本文档与源码位于仓库 packages/shader-transitions(当前版本 0.8.29,见 package.json)。

一、包定位与核心思想

@hyperframes/shader-transitions 解决的问题很具体:在一个由多个 HTML 场景(如 introdemooutro)组成的 HyperFrames 视频中,相邻场景之间需要一个持续的、在动的转场,而不是硬切。

包的核心思路可以概括为三步,入口实现见 hyper-shader.ts

  1. 预捕获(pre-capture):为每个转场在其开始前的时刻捕获"出境场景"的动画采样帧,同时捕获"入境场景"从转场起点继续推进的采样帧;
  2. 合成(composite):播放进入转场窗口时,把两套缓存帧作为纹理(texture)上传到 WebGL,交给片元着色器按 u_progress 混合输出;
  3. 时间线(timeline)init() 返回一个 GSAP 时间线。转场期间场景动画依然持续向前推进,但播放循环中不再有 DOM 捕获开销;转场结束后由原本的场景动画无缝接管。

由此带来的关键收益是:转场过程是真正"在动"的(captured animation keeps advancing),且 WebGL 渲染发生在 GPU 上;如果浏览器没有 WebGL,包会自动回退为普通时间线播放(不做着色器合成)。源码中对应回退逻辑会打印 [HyperShader] WebGL unavailable — shader transitions disabled. 并直接返回注册好的时间线(hyper-shader.tsinit() 内)。

二、安装与三种加载方式

推荐通过 npm 安装:

npm install @hyperframes/shader-transitions

或者通过 CDN 以 <script> 标签直接加载 IIFE 产物:

<script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions/dist/index.global.js"></script>

该包设计上"自包含、可独立分发":源码注释明确指出它作为独立 CDN bundle 发布,不依赖 @hyperframes/engine(相关说明见 hyper-shader.ts)。其唯一运行时依赖是 html2canvas(^1.4.1,见 package.json),并在构建时通过 noExternal: ["html2canvas"] 打进了产物。

产物由 tsup.config.ts 生成,三种格式及适用场景如下表:

格式 文件 适用场景 全局变量
ESM dist/index.js 打包器(Vite、webpack 等)
CJS dist/index.cjs Node.js / require()
IIFE dist/index.global.js <script> 标签、CDN HyperShader

所有格式均包含 source map,并随包发布 TypeScript 类型声明(tsup 开启了 dts: true)。IIFE 场景下请注意源码注释提到的一点:当通过 <script> 手工加载 bundle 后,从 vanilla JS 传入显式的空字符串 shader: "" 不会被当作"省略",而会走到着色器注册表并抛出明确的 unknown shader 错误,这是刻意的严格行为(见 registry.tsgetFragSource())。

三、核心 API:init(config): GsapTimeline

所有能力都收敛到 init() 一个函数。基本用法:

import { init } from "@hyperframes/shader-transitions";

const tl = init({
  bgColor: "#0a0a0a",        // 场景捕获时的兜底背景色
  accentColor: "#ff6b2b",    // 着色器辉光效果强调色
  scenes: ["scene-1", "scene-2", "scene-3"],
  transitions: [
    { time: 3, shader: "domain-warp", duration: 0.8 },
    { time: 8, shader: "light-leak", duration: 0.7 },
  ],
});

3.1 配置项全表

选项 类型 必填 说明
bgColor string 场景捕获时的兜底背景色(hex)。应使用组合的 body/canvas 背景色——每个场景通过 CSS 自行设置各自的 background-color
accentColor string 着色器辉光效果的强调色(hex)
scenes string[] 各场景元素的 ID,按顺序排列
transitions TransitionConfig[] 转场定义数组,见下文
timeline GsapTimeline 已有的 GSAP 时间线,把转场叠加到它上面
compositionId string 覆盖 data-composition-id,用于时间线注册
previewCaptureFps number 浏览器预览模式每秒钟为每个转场预捕获的采样帧数。默认 30;渲染模式则改为确定性的逐帧合成,不使用此值

3.2 底层行为印证

对照 hyper-shader.tsinit() 实现,可以确认以下细节:

  • 组合画布尺寸:优先读取根元素(带 data-composition-id 的元素)上的 data-width / data-height 属性;缺失或非法时回退到 1920 × 1080(常量 DEFAULT_WIDTH/DEFAULT_HEIGHT 定义于 webgl.ts)。compositionId 的解析顺序是 config.compositionId → 根元素 data-composition-id"main"
  • 强调色三档化:单个 accentColor 会在内部被推导成三档 RGB 用于片元着色器 uniform u_accentu_accent_dark(约乘 0.35)与 u_accent_bright(约 1.5x+0.2 后截断到 1)。未提供时使用默认橙色三档 [1, 0.6, 0.2] / [0.4, 0.15, 0] / [1, 0.85, 0.5]
  • 预览采样速率previewCaptureFps 默认 30,并在取值上被钳制到 1 ~ 60 之间;非法值(NaN/非正数)回退到默认值。
  • WebGL 画布:包会在组合根元素(或 body)下创建一个 id="gl-canvas" 的透明覆盖 canvas,样式为 position:absolute; top:0; left:0; z-index:100; pointer-events:none,尺寸与组合一致;WebGL 上下文开启 preserveDrawingBuffer 以便后续读取。
  • 着色器程序:所有用到的着色器会按名称去重编译并缓存到 programs Map;单个着色器编译失败只打印 [HyperShader] Failed to compile ...,不影响其他转场。
  • 着色器间插值:由于捕获帧是离散的,两个相邻采样帧之间由包内置的一个 mix(texture2D(u_a), texture2D(u_b), u_mix) 混合程序(blend program)做线性插值,配合纹理交错机制保证预览中的转场依然连续。

3.3 组合到已有时间线

如果你的页面已经有自己的 GSAP 时间线(例如已写好每个场景的进入/退出动画),可以把时间线传入,转场会被"叠"到上面而不是另起炉灶:

import { init } from "@hyperframes/shader-transitions";
import { gsap } from "gsap";

const tl = gsap.timeline({ paused: true });
// ... add your scene animations ...

init({
  bgColor: "#000",
  scenes: ["intro", "demo", "outro"],
  transitions: [
    { time: 5, shader: "cinematic-zoom" }, // duration/ease 走默认值
    { time: 12, shader: "glitch", duration: 0.5 },
  ],
  timeline: tl,
});

传入 timeline 时,包不会把返回值重新注册到全局 window.__timelines[compositionId],注册只发生在"由包自建时间线"的情况下(见 hyper-shader.tsregisterTimeline())。

四、TransitionConfigSHADER_NAMES

每个转场由 TransitionConfig 描述:

选项 类型 默认值 说明
time number 转场开始时间(秒)
shader ShaderName 上表中的着色器名。注意源码类型中它是可选的:省略(undefined)时该转场退化为 CSS 交叉淡入淡出,不依赖 WebGL
duration number 0.7 转场持续时长(秒)
ease string "power2.inOut" GSAP 缓动函数

默认值 0.7"power2.inOut" 在两个模式(浏览器预览、引擎确定性渲染)和元数据写入中共用同一组常量(DEFAULT_DURATION/DEFAULT_EASE),保证"预览里怎么动、引擎 seek 时怎么动、producer 读取元数据时按什么参数合成"三者完全一致。

SHADER_NAMES 导出全部着色器名字符串数组,可用于参数校验或构建下拉 UI:

import { SHADER_NAMES } from "@hyperframes/shader-transitions";
// ["domain-warp", "ridged-burn", "whip-pan", ...]

它在源码中由注册表对象的键推导而来:ShaderName = keyof typeof shaders,查找未知名称会抛出 [HyperShader] Unknown shader: "xxx". Available: ...(见 registry.ts)。

4.1 可用着色器一览

着色器 描述
domain-warp 基于噪声的有机扭曲,带发光边缘
ridged-burn 脊状(ridged)噪声灼烧,带火花与热辉光
whip-pan 水平运动模糊,模拟快速甩镜
sdf-iris 圆形光圈擦除(iris wipe),带发光环边
ripple-waves 从中心辐射的同心涟漪扭曲
gravitational-lens 引力透镜式扭曲,带色差
cinematic-zoom 径向缩放模糊,带色边
chromatic-split 从中心向外的 RGB 通道分离
glitch 数字故障,块状位移 + 扫描线
swirl-vortex 基于噪声扭曲的螺旋旋转
thermal-distortion 从画面底部升腾的热浪扰动
flash-through-white 闪白后揭示下一场景
cross-warp-morph 噪声驱动的双场景形变混合
light-leak 暖色电影漏光 + 镜头光晕

4.2 共享着色器基础设施

所有片元着色器并非凭空独立,而是拼接自公共头部,理解这一点有助于二次开发:

  • 顶点着色器把 a_pos-1..1 的四边形)映射为 v_uv 并做 Y 轴翻转,适配 WebGL 坐标系(见 common.ts);
  • 每个片元着色器头部(H 常量)统一声明了这些 uniform:u_from/u_to(出境/入境两张纹理)、u_progress(0→1 进度)、u_resolution(分辨率)、u_accent/u_accent_dark/u_accent_bright(三档强调色);
  • 需要噪声的着色器会拼入 NQ 常量——基于 hash 的 value noise + 五次平滑插值 + 旋转各向异性的 5 层 FBM。

也就是说,"纹理对(u_fromu_to)+ 一个进度标量"是所有转场的通用数据契约;gl_FragColor 的写法是"按进度/噪声混合两帧,再叠加强调色效果"。均匀值由 webgl.tsrenderShader() 统一写入(采样器绑定 TEXTURE0/TEXTURE1,进度、分辨率、三档颜色逐一 uniform*),程序与 uniform 位置都做了缓存以省去重复查询。

五、运行架构:预捕获 → 着色器合成 → GSAP 时间线

浏览器预览模式下,一次转场周期的数据流如下(对应 init() 后半段逻辑,见 hyper-shader.ts):

  1. 初始化时按 scenes.length === transitions.length + 1 校验(例如 3 个场景配 2 个转场),违反会直接抛错;随后逐个检查场景 ID 存在于 DOM 中且带 .scene class,两类问题会分别抛出清晰错误(scene ids not found in DOM: ... / elements found but missing .scene class: ...)。
  2. 为每个转场预编译对应的 GLSL 程序。
  3. 转场前开始预捕获:对出境场景与入境场景按 previewCaptureFps 采样动画帧。捕获结果先以 PNG Blob 形式写入 IndexedDB(见第六节),播放前按需把 Blob 解码成 ImageBitmap/Image 再上传为 WebGL 纹理。
  4. 播放进入转场窗口时,u_progress 随时间线推进(映射到 durationease),着色器每帧对两张纹理做混合,结果直接绘制到覆盖在页面上的 gl-canvas
  5. 播放经过转场窗口后,隐藏 GL 画布,露出持续推进中的入境场景 DOM,画面无缝衔接。

整个过程里,init() 返回的 GsapTimeline 才是组合时间线的"真相来源"——无论转场是着色器合成还是 CSS 交叉淡入淡出,都可以被暂停、seek、play,与 HyperFrames 的既有播放体系兼容。

5.1 WebGL 不可用与 CSS 交叉淡入淡出降级

两条独立的降级路径需要分清:

  • 无 WebGL 环境createContext() 返回空,init() 打印警告并直接返回普通时间线,转场退化为硬切,场景动画完全正常。
  • 某项转场未指定 shadershader === undefined):该转场以 CSS opacity 交叉淡入淡出执行,不需要 WebGL。在引擎渲染模式下,这样的条目会被安排成真实的 opacity tween(详见第七节),保证单帧截图里就包含正确的混合结果。

六、场景捕获管线:从 html2canvas 到原生 HTML-in-Canvas

"把 DOM 场景变成纹理"是整套方案最脆弱也最关键的部分。包支持两条捕获路径,策略代码见 capture.ts

  • 原生 HTML-in-Canvas(首选):当浏览器暴露 Chrome 实验性的 CanvasDrawElement API 时,使用 layoutSubtree canvas + drawElementImage() 直接绘制 DOM。实现细节包括:把场景克隆进一个 position:fixed; z-index:-9999; opacity:0 的 layoutsubtree canvas,等待两个 requestAnimationFrame 让浏览器完成布局/绘制,用 bgColor 填充底色,再 drawElementImage 读出画面并复制到结果 canvas。该路径失败时自动回退到 html2canvas
  • html2canvas 回退html2canvas 抓取时为避免 Safari 的画布污染(SecurityError: The operation is insecure),固定开启 useCORS: trueallowTaint: true。这里有一个值得注意的工程取舍:tainted canvas 无法被 gl.texImage2D 上传(WebGL 规范强制 SecurityError),所以 allowTaint 的实际作用是把"失败点"从 html2canvas 内部挪到更可控的纹理上传处,由调用方统一兜底。此外还提供 foreignObjectRendering 尝试开关(失败自动回退到常规渲染)、onclone 中把带 transform 的 box-shadow 抽取成独立 shim 元素(因为变换会破坏阴影栅格化)、强制克隆场景可见等处理。

isHtmlInCanvasCaptureSupported() 可以自行做特性检测(源码判定"存在 layoutSubtree 属性 + 2D 上下文具备 drawElementImage 函数"),对应测试见 capture.test.ts(验证了非浏览器环境返回 false、能力齐备返回 true、缺 drawElementImage 返回 false 三种情况)。

另外,捕获对零尺寸 pattern 有个 Safari 相关防御补丁:重写 CanvasRenderingContext2D.prototype.createPattern,当传入 0×0 的 canvas 时返回 null 而不是让浏览器抛错(initCapture())。

6.1 浏览器预览快照的 IndexedDB 缓存

每次刷新页面都重新捕获几十上百帧显然不划算,因此浏览器预览的快照会被持久化:

  • 数据库名为 hyper-shader-preview-cache,object store 名为 frames,schema 版本 v1(常量见 hyper-shader.ts)。
  • 缓存键由 composition ID、场景 DOM/样式签名、转场时序、捕获 FPS、缩放与画布尺寸 综合推导。DOM/样式签名不是简单 hash:它基于文档内所有 <style> 文本、<link rel=stylesheet> 及脚本特征的 stableHash,场景自身的签名还会在计算前剔除播放过程中被运行时改写的 opacity/visibility/pointer-events 等内联样式,从而让缓存身份追踪"作者写的内容"而非"上次预览的播放头状态"。
  • 刷新后若键匹配,快照直接加载为 WebGL 纹理,不再重捕。
  • 运行中编辑场景或样式表时,只会把相邻转场的缓存标记为 dirty,重捕推迟到真正播放到那个转场时才发生,保证编辑器操作期间交互不卡顿。
  • 缓存总量上限为 1200 条(MAX_SNAPSHOT_CACHE_ENTRIES),写入时按 updatedAt 淘汰最旧条目并清理当前 composition 的失效键。

6.2 预捕获阶段的加载反馈

首次播放前的快照准备可能需要一段时间,包内置了一套全屏加载反馈:覆盖层包含品牌图形、进度短语与逐 transition / 逐 frame 的进度数字,短语按进度切换("Preparing scene transitions"、"Sampling outgoing scene motion" 等)。该覆盖层带 data-hyperframes-ignoredata-no-capturedata-no-pick 等标记,确保它不会污染捕获与拾取逻辑。

播放器接管 vs 内置加载 UI:当页面由 <hyperframes-player> 承载时,浏览器预览的捕获缩放与转场预加载 UI 的所有权归属播放器(对应属性 shader-capture-scaleshader-loading),而不是组合代码;非播放器的直接预览则保留内置的全保真加载兜底。实现上,捕获缩放系数读取全局变量 __HF_SHADER_CAPTURE_SCALE 或查询参数 __hf_shader_capture_scale(解析后钳制在 0.25 ~ 1,默认 1),加载模式读取 __HF_SHADER_LOADING__hf_shader_loading,取值 player/true → 播放器接管、none/false/off → 关闭、其余 → internal 内置覆盖层。

七、引擎渲染模式:确定性逐帧输出

浏览器里人眼看 30fps 预捕获足够,但视频渲染(引擎)要求每一帧都精确确定。init() 会探测 window.__HF_VIRTUAL_TIME__ 标记(引擎在渲染模式注入的虚拟时间 shim,见 hyper-shader.ts),一旦检测到就切换到 initEngineMode()完全跳过所有 GL / canvas / html2canvas 分支,只构建一条确定性的"透明度翻转"时间线:

  • 非首场景初始全部 opacity: 0(用 tl.set(..., 0) 挂进时间线开头,保证逆向 seek 也能恢复正确初态)。
  • 对着色器转场:转场窗口内 from/to 两场景都保持 opacity: 1,出境场景在 time + duration 时刻降到 0——这样引擎的 Node 端分层合成器能分别独立捕获两场景再自行混合。
  • 对 CSS 交叉淡入淡出:安排真实的 opacity tween(fromTo),保证单帧页面截图本身已包含正确的混合结果。
  • 使用 tl.set()(零时长 tween)而不是 tl.call(),因为 tl.call 只在运动方向上触发,引擎 warmup 会正向 seek 到各转场起点、随后又反向 seek 回 t=0,回调态会卡住而 set 可随反向 seek 正确还原。

引擎读取合成的依据是 init() 同步写入 window.__hf.transitions 的元数据数组(每项含 timedurationshadereasefromScenetoScene,缺省 duration/ease 时同样使用 0.7 / power2.inOut)。该结构刻意在包内本地重声明(不 import engine 的类型)以保持 CDN 独立,并与 engine 的 HfTransitionMeta 保持同步(注释中明确说明)。

7.1 可选的页面端合成器(engine-mode page compositing)

当 producer 以 EngineConfig.enablePageSideCompositing: true 启动并注入 window.__HF_PAGE_SIDE_COMPOSITING__ 哨兵时,引擎模式还会安装一个页面端 WebGL 合成器(installPageSideCompositor(),导出见 index.ts,实现见 engineModePageComposite.ts),让"单次整页截图"也能得到与预览路径一致的原生保真捕获。它采用两阶段协议:

  1. Phase 1(seek 包装):包装 window.__hf.seek。进入转场窗口时,把 FROM/TO 场景克隆进两个常驻的 layoutsubtree staging canvas,并设 window.__hf_page_composite_pending
  2. Paint force(引擎侧):引擎检测到 pending 标记后触发一次微型的 Page.captureScreenshot,强制浏览器合成器把 staging canvas 的克隆绘制出来。
  3. Phase 2(resolve):引擎调用 window.__hf_page_composite_resolve,用 drawElementImage 从已绘制克隆读出画面、上传纹理、跑着色器并显示 GL 覆盖层,最后清理 staging。

克隆时会把各自 getBoundingClientRect() 实测到的盒模型 left/top/width/height 固定到克隆上——这是为了规避"仅靠 inset:0 定位的场景克隆进 layout subtree 后坍缩成 0×0"的已知问题;同时强制克隆可见、解码 data-URI 图片,避免 GSAP 残留的 opacity:0/hidden data-start 导致 Chrome 没有 paint record。isPageSideCompositingSupported() 会先探测 drawElementImage + WebGL 都可用才安装,否则告警并退回 opacity-flip(由 Node 端分层管线完成混合)。

八、常见接入错误与自查清单

结合源码中的显式校验,接入时最容易踩的坑可以提前规避:

  • 场景数不匹配scenes 必须比 transitions 恰好多 1(init() 会直接抛错并给出二者实际长度)。
  • 场景元素不存在或缺 .scene classinit() 会逐个校验并抛出具体 ID 列表;捕获与合成后续都假定这两个前提成立,提前校验避免了"纹理拿到过期 ID、转场悄悄失效"。
  • 着色器名拼错:注册表抛 [HyperShader] Unknown shader,并在错误信息里列出全部可用名。
  • shader: ""shader: undefined 语义不同:省略字段 = CSS 交叉淡入淡出;显式空串 = 编译期报错(故意的严格行为)。
  • WebGL 不可用:不会白屏,只是失去着色器转场(硬切 + 场景动画正常)。
  • bgColor 与场景底色bgColor 只作捕获时的兜底填充,场景应各自用 CSS background-color 声明;两者不匹配会在捕获帧边缘露底。

九、与 HyperFrames 生态的关系

着色器转场包是组合作者直接使用的装配层,其上下游生态如下(相关目录见仓库根):

  • @hyperframes/core:类型、解析器与运行时——决定组合如何被解释成时间线;
  • @hyperframes/engine:渲染引擎——负责确定性逐帧输出,与本节介绍的 engine-mode 元数据(window.__hf.transitions)对接;
  • hyperframes CLI:命令行渲染/预览入口,串起"写 HTML → 渲染视频"的全流程。

仓库的 14 个内置着色器名(domain-warpridged-burnwhip-panlight-leak 等)与其视觉语言也可以从 registry/blocks 下的同名 block(如 domain-warp-dissolveridged-burnlight-leakwhip-pan)获得参照——这些 block 正对应同类转场观感,可作为挑选或对齐场景间过渡风格的参考。

若需要更多用户态资料,@hyperframes/shader-transitions 属于 engine 之上、composition 之内的工具库;在本仓库中以它命名的组成与用法最佳落点仍是各 block 的 HTML + 元数据 JSON 配对,例如 registry/blocks/ridged-burnregistry/blocks/light-leak

结语

@hyperframes/shader-transitions 把"DOM 场景动画"与"GLSL 片元着色器"两种截然不同的渲染范式粘合起来:浏览器预览走"预捕获 + 纹理合成 + GSAP 时间线",满足交互流畅度;引擎渲染走"透明度元数据 + 确定性逐帧 + 可选页面端合成器",满足视频输出的一致性。理解它的两级降级(无 WebGL、无 shader 字段)、两条捕获路径(原生 HTML-in-Canvas 与 html2canvas)以及 IndexedDB 快照缓存,是把它稳定用进 HyperFrames 组合的前提。

本文所述包采用 MIT 许可证(见 package.json 与仓库根 LICENSE 约定),可在 npm 上以 @hyperframes/shader-transitions 独立使用。

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

项目优选

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