<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: TommyDee</title>
    <description>The latest articles on Forem by TommyDee (@thomasdolso).</description>
    <link>https://forem.com/thomasdolso</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3935266%2Fcd3d3274-7276-4242-803d-0c0e1970ca4b.jpg</url>
      <title>Forem: TommyDee</title>
      <link>https://forem.com/thomasdolso</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/thomasdolso"/>
    <language>en</language>
    <item>
      <title>Migrating from gl-transitions to @vysmo/transitions: the diff that matters</title>
      <dc:creator>TommyDee</dc:creator>
      <pubDate>Mon, 18 May 2026 21:53:33 +0000</pubDate>
      <link>https://forem.com/thomasdolso/migrating-from-gl-transitions-to-vysmotransitions-the-diff-that-matters-g82</link>
      <guid>https://forem.com/thomasdolso/migrating-from-gl-transitions-to-vysmotransitions-the-diff-that-matters-g82</guid>
      <description>&lt;p&gt;If you've ever wanted a fancy crossfade between two images on the web, you've probably ended up at &lt;a href="https://gl-transitions.com/" rel="noopener noreferrer"&gt;gl-transitions&lt;/a&gt;. 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.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://vysmo.com/transitions" rel="noopener noreferrer"&gt;&lt;code&gt;@vysmo/transitions&lt;/code&gt;&lt;/a&gt; — same conceptual model, modernized surface — and the diff is small enough to do in an afternoon and big enough to be worth writing down.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fitblwsh7tuuvd25dbeml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fitblwsh7tuuvd25dbeml.png" alt="WebGl Transition" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model is the same
&lt;/h2&gt;

&lt;p&gt;Both libraries treat a transition as a GLSL fragment shader that samples two textures and a &lt;code&gt;progress&lt;/code&gt; uniform, and writes a color. You drive &lt;code&gt;progress&lt;/code&gt; from 0 to 1 over some duration and call render every frame.&lt;/p&gt;

&lt;p&gt;That's it. If you understood gl-transitions, you understand vysmo. The differences are surface-level — until they aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-line diff
&lt;/h2&gt;

&lt;p&gt;Here's the same crossfade, in both libraries, side by side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before — gl-transitions&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createTransition&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gl-transitions/lib/transition.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;GL&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gl-transitions/transitions/wind.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webgl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromTex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toTex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After — @vysmo/transitions&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wind&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vysmo/transitions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visually similar. But notice what disappeared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You no longer get the WebGL context yourself.&lt;/li&gt;
&lt;li&gt;You no longer create textures by hand and pass them as &lt;code&gt;fromTex&lt;/code&gt; / &lt;code&gt;toTex&lt;/code&gt; — you pass &lt;code&gt;HTMLImageElement&lt;/code&gt; (or canvas, video, ImageBitmap, OffscreenCanvas, ImageData) and the library uploads, caches, and reuses them.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;{ size: 0.2 }&lt;/code&gt; is type-checked. Misspell it and your editor underlines it before you save.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createTransition(gl, GL)&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you gain (the bits that mattered for me)
&lt;/h2&gt;

&lt;p&gt;Five things. In rough order of how often they showed up while I was porting.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. TypeScript inference on every transition's params
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;defaults&lt;/code&gt; object and &lt;code&gt;params&lt;/code&gt; is &lt;code&gt;Partial&amp;lt;typeof transition.defaults&amp;gt;&lt;/code&gt;. Editor autocomplete tells you &lt;code&gt;wind&lt;/code&gt; takes a &lt;code&gt;size&lt;/code&gt; and &lt;code&gt;direction&lt;/code&gt;, and what the value ranges are.&lt;/p&gt;

&lt;p&gt;You never write &lt;code&gt;Transition&amp;lt;{...}&amp;gt;&lt;/code&gt; by hand. Types are inferred from the data.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Tree-shaking per shader
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@vysmo/transitions&lt;/code&gt; exports each transition as a named export from the package root. Import &lt;code&gt;paintBleed&lt;/code&gt; and &lt;code&gt;crossZoom&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Sources aren't textures anymore
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Live video as the from-side. The Runner re-uploads its pixels per render() call.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLVideoElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crossZoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imgB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Mesh and multi-pass shaders work
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;vysmo's &lt;code&gt;Runner&lt;/code&gt; builds a vertex buffer and runs &lt;code&gt;drawArraysInstanced&lt;/code&gt; for mesh transitions, and allocates ping-pong framebuffers for multi-pass shaders that need to read their previous output. Same &lt;code&gt;render()&lt;/code&gt; call from your perspective — the difference is internal.&lt;/p&gt;

&lt;p&gt;In practice this means &lt;code&gt;pageCurl&lt;/code&gt;, &lt;code&gt;polygonFlip&lt;/code&gt;, &lt;code&gt;glassShatter&lt;/code&gt;, &lt;code&gt;inkDiffuse&lt;/code&gt;, &lt;code&gt;lenticularFlip&lt;/code&gt;, &lt;code&gt;tileScatter&lt;/code&gt; all exist as built-ins. None of those are possible as pure fragment shaders.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpv5ahn0x2yajczel20sd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpv5ahn0x2yajczel20sd.png" alt="PageFlip Transition" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Endpoint correctness is enforced
&lt;/h3&gt;

&lt;p&gt;Every built-in is tested to produce pixel-pure &lt;code&gt;from&lt;/code&gt; at &lt;code&gt;progress=0&lt;/code&gt; and pixel-pure &lt;code&gt;to&lt;/code&gt; at &lt;code&gt;progress=1&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;If you author your own with &lt;code&gt;defineTransition&lt;/code&gt;, the same invariant applies — and the docs lay out the three rules that make it work. Worth a separate post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Porting a custom shader
&lt;/h2&gt;

&lt;p&gt;If you wrote your own gl-transitions shader, the GLSL body usually ports as-is. Drop it into &lt;code&gt;defineTransition&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineTransition&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vysmo/transitions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myWind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineTransition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-wind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;glsl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    uniform float uSize;
    vec4 transition(vec2 uv) {
      float r = fract(sin(uv.y * 1000.0) * 1000.0);
      float m = 1.0 - smoothstep(-uSize, 0.0, uv.x - uProgress * (1.0 + uSize));
      return mix(getFromColor(uv), getToColor(uv), m * (0.3 + 0.7 * r));
    }
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myWind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The renamings you'll do mechanically:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gl-transitions&lt;/th&gt;
&lt;th&gt;@vysmo/transitions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uProgress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getFromColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getFromColor&lt;/code&gt; &lt;em&gt;(same)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getToColor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getToColor&lt;/code&gt; &lt;em&gt;(same)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ratio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;derive from &lt;code&gt;uResolution&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;custom uniform &lt;code&gt;size&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;uniform &lt;code&gt;uSize&lt;/code&gt;, key &lt;code&gt;size&lt;/code&gt; in &lt;code&gt;defaults&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Naming conventions: transition names are kebab-case, export identifiers are camelCase, custom uniforms in GLSL are uPascalCase, mapped automatically from camelCase keys in &lt;code&gt;defaults&lt;/code&gt;. So &lt;code&gt;defaults.noiseStrength&lt;/code&gt; becomes &lt;code&gt;uniform float uNoiseStrength;&lt;/code&gt; in your shader.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up
&lt;/h2&gt;

&lt;p&gt;Honest list, not a sales pitch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebGL1 support.&lt;/strong&gt; Vysmo is WebGL2-only. Modern browsers all ship it (Safari 15+, iOS 15+, Firefox 51+, Chrome 56+), but if you support ancient mobile, you'll need a CSS opacity-crossfade fallback wrapped in a &lt;code&gt;try/catch&lt;/code&gt; around the &lt;code&gt;Runner&lt;/code&gt; constructor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The community gallery model.&lt;/strong&gt; gl-transitions has a huge community-contributed shader library on a single registry page. Vysmo ships 60 transitions, curated, with parameters and tested invariants — and a &lt;code&gt;defineTransition&lt;/code&gt; API for your own. Different philosophy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maturity.&lt;/strong&gt; gl-transitions has been around since 2016. Vysmo shipped this year. There's no Stack Overflow long-tail yet.
If those tradeoffs aren't deal-breakers, the migration is roughly an afternoon for a single transition, a day for a slideshow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The minimal port: do this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;pnpm add @vysmo/transitions @vysmo/animations&lt;/code&gt; (drop &lt;code&gt;@vysmo/animations&lt;/code&gt; if you have your own driver — GSAP, anime.js, raw rAF, scroll progress, anything that produces a 0→1 number works).&lt;/li&gt;
&lt;li&gt;Replace your &lt;code&gt;createTransition(gl, GLSHADER)&lt;/code&gt; calls with the corresponding named import: &lt;code&gt;import { wind } from "@vysmo/transitions"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;new Runner({ canvas })&lt;/code&gt; for the canvas; remove your manual &lt;code&gt;gl = canvas.getContext("webgl")&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;transition.draw(progress, fromTex, toTex, w, h, params)&lt;/code&gt; with &lt;code&gt;runner.render(transition, { from, to, progress, params })&lt;/code&gt;. Pass your &lt;code&gt;HTMLImageElement&lt;/code&gt; directly — drop the texture-creation code.&lt;/li&gt;
&lt;li&gt;On unmount, call &lt;code&gt;runner.dispose()&lt;/code&gt;. (gl-transitions leaks the context if you forget; vysmo also leaks it if you forget. The difference is vysmo gives you one method to call instead of context teardown + program deletion + texture cleanup.)
That's it. Same shaders, same look, smaller bundle, types.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  One philosophical note
&lt;/h2&gt;

&lt;p&gt;The thing that sold me on the migration wasn't the type safety or the tree-shaking — those are table stakes. It was that &lt;strong&gt;&lt;code&gt;runner.render()&lt;/code&gt; is idempotent&lt;/strong&gt;. You pass the current progress on every frame and the library draws. There's no animation loop inside the library, no internal timer, no playback state to fight.&lt;/p&gt;

&lt;p&gt;That means scroll progress drives a transition exactly the same way a &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop does, which is exactly the same way a video editor's timeline scrubber does, which is exactly the same way an export-to-MP4 frame iterator does. Same shader, four use cases, zero code changes. gl-transitions could technically work this way too — but its API hints toward a more imperative model and most people end up wrapping it in their own loop.&lt;/p&gt;

&lt;p&gt;Idempotent render + plain-data transitions is a small architectural decision with surprisingly long reach. It's the part of the library I'd port to my own code even if I weren't using vysmo.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Vysmo libraries are MIT, free, all of them — &lt;a href="https://github.com/vysmodev/vysmo" rel="noopener noreferrer"&gt;github.com/vysmodev/vysmo&lt;/a&gt;&lt;/strong&gt;. Docs, the full transition catalog with live parameter playgrounds, and a Next.js guide at &lt;a href="https://vysmo.com" rel="noopener noreferrer"&gt;vysmo.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webgl</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>animation</category>
    </item>
    <item>
      <title>I just open-sourced 13 MIT libraries for web visual effects</title>
      <dc:creator>TommyDee</dc:creator>
      <pubDate>Sat, 16 May 2026 17:50:36 +0000</pubDate>
      <link>https://forem.com/thomasdolso/i-just-open-sourced-13-mit-libraries-for-web-visual-effects-2g86</link>
      <guid>https://forem.com/thomasdolso/i-just-open-sourced-13-mit-libraries-for-web-visual-effects-2g86</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzh71jtnj8ny92gfju54s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzh71jtnj8ny92gfju54s.webp" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After months of work, today I shipped &lt;strong&gt;Vysmo&lt;/strong&gt; — a set of MIT-licensed libraries for web visual computing. All 13 packages are now on npm under &lt;code&gt;@vysmo&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/transitions&lt;/code&gt;&lt;/strong&gt; — 60 WebGL2 transition shaders defined as plain data. Includes a mesh-based page-curl with drag-scrub mid-flip, polygon flip, and classic crossfades/wipes. Tree-shakable to the byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/text&lt;/code&gt;&lt;/strong&gt; — Multi-property choreographed text animation with 300+ presets. Grapheme-safe splitting via &lt;code&gt;Intl.Segmenter&lt;/code&gt; works for emoji, Arabic, and Devanagari.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/effects&lt;/code&gt;&lt;/strong&gt; — WebGL filter primitives (blur, bloom, glow, vignette, chromatic aberration, color grading).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/easings&lt;/code&gt;&lt;/strong&gt; — 40+ named curves, parametric builders (spring, bezier, wiggle, rough), composition modifiers, CSS export, reduced-motion helpers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/scroll&lt;/code&gt;&lt;/strong&gt; — scroll-driven primitives that compose with transitions and effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/flipbook&lt;/code&gt;&lt;/strong&gt; — drag-scrub page-flip component built on the page-curl shader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/slideshow&lt;/code&gt;&lt;/strong&gt; — image slideshow with opt-in chrome, drives any of the 60 transitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/animations&lt;/code&gt;&lt;/strong&gt; — value-based tweening: &lt;code&gt;animate()&lt;/code&gt;, &lt;code&gt;spring()&lt;/code&gt;, &lt;code&gt;timeline()&lt;/code&gt;, &lt;code&gt;interpolate()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vysmo/gl-core&lt;/code&gt;&lt;/strong&gt; — shared WebGL2 plumbing.&lt;/li&gt;
&lt;li&gt;Plus React wrappers: &lt;code&gt;@vysmo/transitions-react&lt;/code&gt;, &lt;code&gt;@vysmo/text-react&lt;/code&gt;, &lt;code&gt;@vysmo/flipbook-react&lt;/code&gt;, &lt;code&gt;@vysmo/slideshow-react&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Design principles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime dependencies&lt;/strong&gt; in every package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR-safe at module load&lt;/strong&gt; — enforced by a Node import test per package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless-first&lt;/strong&gt; — components are opt-in wrappers around a vanilla TS core. The same code drives canvas, image, and video sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain-data API&lt;/strong&gt; for transitions, text, and effects so the same definition can drive DOM today and a canvas renderer later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick example
&lt;/h2&gt;

&lt;p&gt;Crossfade between two images with one of the 60 WebGL transitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paintBleed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vysmo/transitions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;animate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vysmo/animations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromImg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toImg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;fromImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/photo-a.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;toImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/photo-b.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;fromImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;toImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

&lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paintBleed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fromImg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toImg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transition is just plain data describing how to interpolate between two textures. The Runner handles WebGL plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live playgrounds for every library&lt;/strong&gt;: &lt;a href="https://vysmo.com" rel="noopener noreferrer"&gt;https://vysmo.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://github.com/vysmodev/vysmo" rel="noopener noreferrer"&gt;https://github.com/vysmodev/vysmo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/org/vysmo" rel="noopener noreferrer"&gt;https://www.npmjs.com/org/vysmo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the first npm release (0.1.0), so APIs may still shift before 1.0. I'd love feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API design&lt;/li&gt;
&lt;li&gt;Bugs or weird behavior in the playgrounds&lt;/li&gt;
&lt;li&gt;Demos or use-cases you'd want to see covered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webgl</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
