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

📅 发布时间:2026/9/9 22:08:20
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/hyperframesHyperFrames 遵循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混合输出时间线timelineinit()返回一个 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 srchttps://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.jsscript标签、CDNHyperShader所有格式均包含 source map并随包发布 TypeScript 类型声明tsup开启了dts: true。IIFE 场景下请注意源码注释提到的一点当通过script手工加载 bundle 后从 vanilla JS 传入显式的空字符串shader: 不会被当作省略而会走到着色器注册表并抛出明确的 unknown shader 错误这是刻意的严格行为见 registry.ts 的getFragSource()。三、核心 APIinit(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-coloraccentColorstring否着色器辉光效果的强调色hexscenesstring[]是各场景元素的 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_accent、u_accent_dark约乘 0.35与u_accent_bright约1.5x0.2后截断到 1。未提供时使用默认橙色三档[1, 0.6, 0.2]/[0.4, 0.15, 0]/[1, 0.85, 0.5]。预览采样速率previewCaptureFps默认 30并在取值上被钳制到1 ~ 60之间非法值NaN/非正数回退到默认值。WebGL 画布包会在组合根元素或body下创建一个idgl-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描述选项类型默认值说明timenumber—转场开始时间秒shaderShaderName—上表中的着色器名。注意源码类型中它是可选的省略undefined时该转场退化为 CSS 交叉淡入淡出不依赖 WebGLdurationnumber0.7转场持续时长秒easestringpower2.inOutGSAP 缓动函数默认值0.7与power2.inOut在两个模式浏览器预览、引擎确定性渲染和元数据写入中共用同一组常量DEFAULT_DURATION/DEFAULT_EASE保证预览里怎么动、引擎 seek 时怎么动、producer 读取元数据时按什么参数合成三者完全一致。SHADER_NAMES导出全部着色器名字符串数组可用于参数校验或构建下拉 UIimport { 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常量统一声明了这些 uniformu_from/u_to出境/入境两张纹理、u_progress0→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()打印警告并直接返回普通时间线转场退化为硬切场景动画完全正常。某项转场未指定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: 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-cacheobject store 名为framesschema 版本v1常量见 hyper-shader.ts。缓存键由composition ID、场景 DOM/样式签名、转场时序、捕获 FPS、缩放与画布尺寸综合推导。DOM/样式签名不是简单 hash它基于文档内所有style文本、link relstylesheet及脚本特征的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 tweenfromTo保证单帧页面截图本身已包含正确的混合结果。使用tl.set()零时长 tween而不是tl.call()因为tl.call只在运动方向上触发引擎 warmup 会正向 seek 到各转场起点、随后又反向 seek 回 t0回调态会卡住而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 1seek 包装包装window.__hf.seek。进入转场窗口时把 FROM/TO 场景克隆进两个常驻的 layoutsubtree staging canvas并设window.__hf_page_composite_pending。Paint force引擎侧引擎检测到 pending 标记后触发一次微型的Page.captureScreenshot强制浏览器合成器把 staging canvas 的克隆绘制出来。Phase 2resolve引擎调用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),仅供参考