Migrating from gl-transitions to @vysmo/transitions: the diff that matters
Migrating from gl-transitions to @vysmo/transitions: the diff that matters
If you’ve ever wanted a fancy crossfade between two images on the web, you’ve probably ended up at gl-transitions. It’s been the de facto WebGL transition library for the better part of a decade — a community gallery of GLSL fragment shaders that mix two textures over a progress value from 0 to 1. It works. I shipped with it. You probably shipped with it. But it’s a WebGL1, untyped, class-based draw function bound to a context you manage by hand. In 2026 the rough edges show. So a few weeks ago I migrated a project to @vysmo/transitions — same conceptual model, modernized surface — and the diff is small enough to do in an afternoon and big enough to be worth writing down. This isn’t a “vysmo is better, install it” post. It’s a “here’s the diff, here’s what changes, here’s what doesn’t” post. Decide for yourself.
如果你曾经想在网页上实现两张图片之间炫酷的交叉淡入淡出效果,你很可能用过 gl-transitions。在过去十年里,它一直是事实上的 WebGL 转场库标准——一个由社区维护的 GLSL 片段着色器库,通过 0 到 1 的进度值将两张纹理混合在一起。它确实好用,我用过,你可能也用过。但它是一个基于 WebGL1、无类型、基于类的绘制函数,且绑定在你手动管理的上下文上。到了 2026 年,这些粗糙的边缘开始显现。所以几周前,我将一个项目迁移到了 @vysmo/transitions——概念模型相同,但界面更现代化——迁移差异小到一下午就能完成,但又大到值得记录下来。这不是一篇“vysmo 更好,快去安装”的文章,而是一篇“这是差异,这是变化,这是不变”的文章。请自行判断。
The mental model is the same. Both libraries treat a transition as a GLSL fragment shader that samples two textures and a progress uniform, and writes a color. You drive progress from 0 to 1 over some duration and call render every frame. That’s it. If you understood gl-transitions, you understand vysmo. The differences are surface-level — until they aren’t.
其思维模型是一样的。两个库都将转场视为一个 GLSL 片段着色器,它采样两张纹理和一个进度 uniform,并输出颜色。你只需在一段时间内将进度从 0 驱动到 1,并在每一帧调用渲染即可。就是这样。如果你理解 gl-transitions,你就理解 vysmo。这些差异仅停留在表面——直到你深入使用时才会发现不同。
The five-line diff
五行代码的差异
Here’s the same crossfade, in both libraries, side by side. 这是两个库中实现相同交叉淡入淡出的对比:
// Before — gl-transitions
import createTransition from "gl-transitions/lib/transition.js";
import GL from "gl-transitions/transitions/wind.js";
const gl = canvas.getContext("webgl");
const transition = createTransition(gl, GL);
transition.draw(progress, fromTex, toTex, w, h, { size: 0.2 });
// After — @vysmo/transitions
import { Runner, wind } from "@vysmo/transitions";
const runner = new Runner({ canvas });
runner.render(wind, { from, to, progress, params: { size: 0.2 } });
Visually similar. But notice what disappeared: You no longer get the WebGL context yourself. You no longer create textures by hand and pass them as fromTex / toTex — you pass HTMLImageElement (or canvas, video, ImageBitmap, OffscreenCanvas, ImageData) and the library uploads, caches, and reuses them. The { size: 0.2 } is type-checked. Misspell it and your editor underlines it before you save. createTransition(gl, GL) is gone. There’s no per-shader instance to construct, dispose, or cache. The Runner owns the program cache. That’s roughly 80% of the migration work right there.
视觉上很相似。但请注意消失了什么:你不再需要自己获取 WebGL 上下文。你不再需要手动创建纹理并将其作为 fromTex / toTex 传递——你只需传入 HTMLImageElement(或 canvas、video、ImageBitmap、OffscreenCanvas、ImageData),库会自动上传、缓存并复用它们。{ size: 0.2 } 是经过类型检查的。如果拼写错误,编辑器会在你保存前就标出下划线。createTransition(gl, GL) 消失了。没有了需要构建、销毁或缓存的着色器实例。Runner 负责管理程序缓存。这几乎占据了迁移工作的 80%。
What you gain (the bits that mattered for me)
你能获得什么(对我而言重要的部分)
Five things. In rough order of how often they showed up while I was porting. 有五点,按我在移植过程中遇到的频率大致排序:
1. TypeScript inference on every transition’s params 1. 每个转场参数的 TypeScript 推断
The single biggest day-to-day improvement. In gl-transitions, params are an untyped object — you check the shader source to find out what’s available and what’s misspelled. In vysmo, every transition exports a typed defaults object and params is Partial<typeof transition.defaults>. Editor autocomplete tells you wind takes a size and direction, and what the value ranges are. You never write Transition<{...}> by hand. Types are inferred from the data.
这是日常开发中最大的改进。在 gl-transitions 中,参数是一个无类型的对象——你必须查看着色器源码才能知道有哪些可用参数以及是否拼写错误。在 vysmo 中,每个转场都导出一个带类型的默认对象,参数类型为 Partial<typeof transition.defaults>。编辑器自动补全会告诉你 wind 接受 size 和 direction,以及值的范围。你永远不需要手动编写 Transition<{...}>,类型是从数据中推断出来的。
2. Tree-shaking per shader 2. 基于着色器的 Tree-shaking
gl-transitions ships ~80 shaders and most bundlers can’t tree-shake them effectively because of how the registry imports them. You end up shipping every shader you don’t use. @vysmo/transitions exports each transition as a named export from the package root. Import paintBleed and crossZoom, and that’s all that goes in your bundle. The whole library is ~5 KB gzipped if you imported everything — most apps ship 1–2 KB.
gl-transitions 包含约 80 个着色器,由于其注册表的导入方式,大多数打包工具无法有效地进行 Tree-shaking。结果是你不得不打包所有你没用到的着色器。@vysmo/transitions 将每个转场作为命名导出从包根目录导出。只导入 paintBleed 和 crossZoom,那么只有它们会被打包。如果全部导入,整个库压缩后约为 5 KB——大多数应用实际只占用 1–2 KB。
3. Sources aren’t textures anymore 3. 源不再是纹理
In gl-transitions you write your own texture upload pipeline. Decode an image, create a texture, set parameters, upload pixels, hand the texture to draw, manage its lifecycle. For a single transition this is fine. For a slideshow rotating through 12 images it gets tedious. In vysmo, sources are anything the browser can draw: image, canvas, video, ImageBitmap, OffscreenCanvas, ImageData. The Runner has a texture cache that treats decoded images as immutable (uploaded once, reused) and re-uploads canvases/videos every frame because their pixels can change. You stop thinking about textures.
在 gl-transitions 中,你需要编写自己的纹理上传流水线:解码图片、创建纹理、设置参数、上传像素、传递纹理进行绘制、管理生命周期。对于单个转场这没问题,但对于轮播 12 张图片的幻灯片来说就很繁琐。在 vysmo 中,源可以是浏览器能绘制的任何东西:image、canvas、video、ImageBitmap、OffscreenCanvas、ImageData。Runner 拥有一个纹理缓存,将解码后的图片视为不可变对象(上传一次,多次复用),并因为像素可能改变,在每一帧重新上传 canvas/video。你从此不再需要考虑纹理细节。
4. Mesh and multi-pass shaders work 4. 网格和多通道着色器支持
This is the one that actually unlocked something for me. Effects like page-curl can’t be done in a fragment shader — they need real geometry, silhouette, depth, self-occlusion. gl-transitions’ shape (one fragment shader, one full-screen quad) rules them out. The community gallery has no working page-curl for this reason. vysmo’s Runner builds a vertex buffer and runs drawArraysInstanced for mesh transitions, and allocates ping-pong framebuffers for multi-pass shaders that need to read their previous output. Same render() call from your perspective — the difference is internal.
这是真正让我感到惊喜的一点。像“翻页(page-curl)”这样的效果无法仅通过片段着色器完成——它们需要真实的几何形状、轮廓、深度和自遮挡。gl-transitions 的结构(一个片段着色器,一个全屏四边形)排除了这些可能性。社区库中没有可用的翻页效果也是这个原因。vysmo 的 Runner 会为网格转场构建顶点缓冲区并运行 drawArraysInstanced,并为需要读取前一帧输出的多通道着色器分配乒乓帧缓冲区。从你的角度看,render() 调用是一样的——区别在于内部实现。
5. Endpoint correctness is enforced 5. 强制执行端点正确性
Every built-in is tested to produce pixel-pure from at progress=0 and pixel-pure to at progress=1. No near-misses, no one-frame flash at the end where blur is still half-applied. It sounds minor until you’ve shipped a transition that ends on a barely-visible artifact and your client emails about it on Tuesday.
每个内置转场都经过测试,确保在 progress=0 时完全呈现 from 图片,在 progress=1 时完全呈现 to 图片。没有近似值,也不会出现转场结束时模糊效果还残留一帧的情况。这听起来是小事,直到你发布了一个在结尾处有微小瑕疵的转场,然后周二收到客户的投诉邮件。
Porting a custom shader
移植自定义着色器
If you wrote your own gl-transitions shader, the GLSL body usually ports as-is. Drop it into defineTransition:
如果你编写过自己的 gl-transitions 着色器,GLSL 代码通常可以直接移植。只需将其放入 defineTransition:
import { defineTransition } from "@vysmo/transitions";
export const myWind = defineTransition({
name: "my-wind",
defaults: { size: 0.2 },
glsl: `
uniform float uSize;
vec4 transition(vec2 uv) {
// ...
}
`
});