news 2026/9/9 22:07:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HyperFrames 场景转场实战:用 @hyperframes/shader-transitions 在 GSAP 时间线上接入 GPU 着色器过渡

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

【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes

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.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 生成,三种格式及适用场景如下表:

格式文件适用场景全局变量
ESMdist/index.js打包器(Vite、webpack 等)
CJSdist/index.cjsNode.js /require()
IIFEdist/index.global.js<script>标签、CDNHyperShader

所有格式均包含 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 配置项全表

选项类型必填说明
bgColorstring场景捕获时的兜底背景色(hex)。应使用组合的 body/canvas 背景色——每个场景通过 CSS 自行设置各自的background-color
accentColorstring着色器辉光效果的强调色(hex)
scenesstring[]各场景元素的 ID,按顺序排列
transitionsTransitionConfig[]转场定义数组,见下文
timelineGsapTimeline已有的 GSAP 时间线,把转场叠加到它上面
compositionIdstring覆盖data-composition-id,用于时间线注册
previewCaptureFpsnumber浏览器预览模式每秒钟为每个转场预捕获的采样帧数。默认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_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以便后续读取。
  • 着色器程序:所有用到的着色器会按名称去重编译并缓存到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())。

四、TransitionConfigSHADER_NAMES

每个转场由TransitionConfig描述:

选项类型默认值说明
timenumber转场开始时间(秒)
shaderShaderName上表中的着色器名。注意源码类型中它是可选的:省略(undefined)时该转场退化为 CSS 交叉淡入淡出,不依赖 WebGL
durationnumber0.7转场持续时长(秒)
easestring"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.ts 的renderShader()统一写入(采样器绑定 TEXTURE0/TEXTURE1,进度、分辨率、三档颜色逐一uniform*),程序与 uniform 位置都做了缓存以省去重复查询。

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

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

  1. 初始化时按scenes.length === transitions.length + 1校验(例如 3 个场景配 2 个转场),违反会直接抛错;随后逐个检查场景 ID 存在于 DOM 中且带.sceneclass,两类问题会分别抛出清晰错误(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 时,使用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: 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"的已知问题;同时强制克隆可见、解码 contenteditable="false">【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:07:07

FastAPI内网部署docs白屏?离线化Swagger UI资源一劳永逸

在实际的后端开发里&#xff0c;明明本地开发环境跑得好好的 FastAPI 项目&#xff0c;一旦部署到内网服务器&#xff0c;打开/docs页面就只剩一片空白&#xff0c;控制台里刷满了红色报错。这个问题的概率非常高&#xff0c;而且几乎每个进入内网环境的团队都会踩上一次。这篇…

作者头像 李华
网站建设 2026/9/9 22:06:43

现代CMake核心实战:依赖图思维与构建疑难排查指南

1. 现代CMake的核心门槛&#xff1a;从"脚本思维"换成"依赖图思维" 很多人用过CMake&#xff0c;但真正把它当成"构建系统"来用&#xff0c;而不是当成"自动执行编译命令的脚本"来用的&#xff0c;其实非常少。你去看一个维护了两年的…

作者头像 李华
网站建设 2026/9/9 22:06:38

如何在 Langflow 流程中使用 Human-in-the-Loop 实现人工确认?

如何在 Langflow 流程中使用 Human-in-the-Loop 实现人工确认&#xff1f; 【免费下载链接】langflow Langflow is a powerful tool for building and deploying AI-powered agents and workflows. 项目地址: https://gitcode.com/GitHub_Trending/la/langflow 如果你在…

作者头像 李华
网站建设 2026/9/9 22:06:10

基于Python+Vue的母婴商城毕业设计系统实现与部署详解

每年到了三四月份&#xff0c;总有学弟学妹来找我看毕业设计&#xff0c;问得最多的就是“学长&#xff0c;有没有一套完整的、能直接跑起来的系统源码”。说实话&#xff0c;网上资源确实多&#xff0c;但能真正让你搞懂原理、能通过答辩、还能说得清每一行代码为什么这么写的…

作者头像 李华
网站建设 2026/9/9 22:05:18

蓝牙5.4安全广播实战:EAD加密与GATT安全级别特征解析

蓝牙5.4核心规格里&#xff0c;真正让广播链路发生质变的不是PHY速率&#xff0c;而是三个容易被拆开讲的东西&#xff1a;加密广播数据、LE GATT安全级别特征、广播编码选择。我在做一个低功耗蓝牙信标项目时&#xff0c;正好把这三块完整过了一遍&#xff0c;从协议栈底层到业…

作者头像 李华
网站建设 2026/9/9 21:58:31

工程別检查内容怎么写?制造业按工序质量检验的落地指南

前阵子接手一个客户投诉&#xff0c;对方说交付的产品里有混料&#xff0c;外观批次和实际规格对不上。我第一反应是去看现场那份检验报表&#xff0c;结果发现根本没法溯源——工序栏只写了“外观确认”&#xff0c;谁来测、测几个、用什么东西测、判定的边界在哪里&#xff0…

作者头像 李华