HyperFrames 场景转场实战:用 @hyperframes/shader-transitions 在 GSAP 时间线上接入 GPU 着色器过渡
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 场景(如 intro、demo、outro)组成的 HyperFrames 视频中,相邻场景之间需要一个持续的、在动的转场,而不是硬切。
包的核心思路可以概括为三步,入口实现见 hyper-shader.ts:
- 预捕获(pre-capture):为每个转场在其开始前的时刻捕获"出境场景"的动画采样帧,同时捕获"入境场景"从转场起点继续推进的采样帧;
- 合成(composite):播放进入转场窗口时,把两套缓存帧作为纹理(texture)上传到 WebGL,交给片元着色器按
u_progress混合输出; - 时间线(timeline):
init()返回一个 GSAP 时间线。转场期间场景动画依然持续向前推进,但播放循环中不再有 DOM 捕获开销;转场结束后由原本的场景动画无缝接管。
由此带来的关键收益是:转场过程是真正"在动"的(captured animation keeps advancing),且 WebGL 渲染发生在 GPU 上;如果浏览器没有 WebGL,包会自动回退为普通时间线播放(不做着色器合成)。源码中对应回退逻辑会打印 [HyperShader] WebGL unavailable — shader transitions disabled. 并直接返回注册好的时间线(hyper-shader.ts 的 init() 内)。
二、安装与三种加载方式
推荐通过 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.ts 的 getFragSource())。
三、核心 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.ts 的 init() 实现,可以确认以下细节:
- 组合画布尺寸:优先读取根元素(带
data-composition-id的元素)上的data-width/data-height属性;缺失或非法时回退到1920 × 1080(常量DEFAULT_WIDTH/DEFAULT_HEIGHT定义于 webgl.ts)。compositionId的解析顺序是config.compositionId→ 根元素data-composition-id→"main"。 - 强调色三档化:单个
accentColor会在内部被推导成三档 RGB 用于片元着色器 uniformu_accent、u_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以便后续读取。 - 着色器程序:所有用到的着色器会按名称去重编译并缓存到
programsMap;单个着色器编译失败只打印[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.ts 的 registerTimeline())。
四、TransitionConfig 与 SHADER_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_from、u_to)+ 一个进度标量"是所有转场的通用数据契约;gl_FragColor 的写法是"按进度/噪声混合两帧,再叠加强调色效果"。均匀值由 webgl.ts 的 renderShader() 统一写入(采样器绑定 TEXTURE0/TEXTURE1,进度、分辨率、三档颜色逐一 uniform*),程序与 uniform 位置都做了缓存以省去重复查询。
五、运行架构:预捕获 → 着色器合成 → GSAP 时间线
浏览器预览模式下,一次转场周期的数据流如下(对应 init() 后半段逻辑,见 hyper-shader.ts):
- 初始化时按
scenes.length === transitions.length + 1校验(例如 3 个场景配 2 个转场),违反会直接抛错;随后逐个检查场景 ID 存在于 DOM 中且带.sceneclass,两类问题会分别抛出清晰错误(scene ids not found in DOM: .../elements found but missing .scene class: ...)。 - 为每个转场预编译对应的 GLSL 程序。
- 转场前开始预捕获:对出境场景与入境场景按
previewCaptureFps采样动画帧。捕获结果先以 PNG Blob 形式写入 IndexedDB(见第六节),播放前按需把 Blob 解码成ImageBitmap/Image再上传为 WebGL 纹理。 - 播放进入转场窗口时,
u_progress随时间线推进(映射到duration与ease),着色器每帧对两张纹理做混合,结果直接绘制到覆盖在页面上的gl-canvas。 - 播放经过转场窗口后,隐藏 GL 画布,露出持续推进中的入境场景 DOM,画面无缝衔接。
整个过程里,init() 返回的 GsapTimeline 才是组合时间线的"真相来源"——无论转场是着色器合成还是 CSS 交叉淡入淡出,都可以被暂停、seek、play,与 HyperFrames 的既有播放体系兼容。
5.1 WebGL 不可用与 CSS 交叉淡入淡出降级
两条独立的降级路径需要分清:
- 无 WebGL 环境:
createContext()返回空,init()打印警告并直接返回普通时间线,转场退化为硬切,场景动画完全正常。 - 某项转场未指定
shader(shader === undefined):该转场以 CSS opacity 交叉淡入淡出执行,不需要 WebGL。在引擎渲染模式下,这样的条目会被安排成真实的 opacity tween(详见第七节),保证单帧截图里就包含正确的混合结果。
六、场景捕获管线:从 html2canvas 到原生 HTML-in-Canvas
"把 DOM 场景变成纹理"是整套方案最脆弱也最关键的部分。包支持两条捕获路径,策略代码见 capture.ts。
- 原生 HTML-in-Canvas(首选):当浏览器暴露 Chrome 实验性的 CanvasDrawElement API 时,使用
layoutSubtreecanvas +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: true与allowTaint: 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-ignore、data-no-capture、data-no-pick 等标记,确保它不会污染捕获与拾取逻辑。
播放器接管 vs 内置加载 UI:当页面由 <hyperframes-player> 承载时,浏览器预览的捕获缩放与转场预加载 UI 的所有权归属播放器(对应属性 shader-capture-scale、shader-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 的元数据数组(每项含 time、duration、shader、ease、fromScene、toScene,缺省 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),让"单次整页截图"也能得到与预览路径一致的原生保真捕获。它采用两阶段协议:
- Phase 1(seek 包装):包装
window.__hf.seek。进入转场窗口时,把 FROM/TO 场景克隆进两个常驻的 layoutsubtree staging canvas,并设window.__hf_page_composite_pending。 - Paint force(引擎侧):引擎检测到 pending 标记后触发一次微型的
Page.captureScreenshot,强制浏览器合成器把 staging canvas 的克隆绘制出来。 - 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()会直接抛错并给出二者实际长度)。 - 场景元素不存在或缺
.sceneclass:init()会逐个校验并抛出具体 ID 列表;捕获与合成后续都假定这两个前提成立,提前校验避免了"纹理拿到过期 ID、转场悄悄失效"。 - 着色器名拼错:注册表抛
[HyperShader] Unknown shader,并在错误信息里列出全部可用名。 shader: ""与shader: undefined语义不同:省略字段 = CSS 交叉淡入淡出;显式空串 = 编译期报错(故意的严格行为)。- WebGL 不可用:不会白屏,只是失去着色器转场(硬切 + 场景动画正常)。
bgColor与场景底色:bgColor只作捕获时的兜底填充,场景应各自用 CSSbackground-color声明;两者不匹配会在捕获帧边缘露底。
九、与 HyperFrames 生态的关系
着色器转场包是组合作者直接使用的装配层,其上下游生态如下(相关目录见仓库根):
- @hyperframes/core:类型、解析器与运行时——决定组合如何被解释成时间线;
- @hyperframes/engine:渲染引擎——负责确定性逐帧输出,与本节介绍的 engine-mode 元数据(
window.__hf.transitions)对接; - hyperframes CLI:命令行渲染/预览入口,串起"写 HTML → 渲染视频"的全流程。
仓库的 14 个内置着色器名(domain-warp、ridged-burn、whip-pan、light-leak 等)与其视觉语言也可以从 registry/blocks 下的同名 block(如 domain-warp-dissolve、ridged-burn、light-leak、whip-pan)获得参照——这些 block 正对应同类转场观感,可作为挑选或对齐场景间过渡风格的参考。
若需要更多用户态资料,@hyperframes/shader-transitions 属于 engine 之上、composition 之内的工具库;在本仓库中以它命名的组成与用法最佳落点仍是各 block 的 HTML + 元数据 JSON 配对,例如 registry/blocks/ridged-burn 与 registry/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 独立使用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00