<?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>DEV Community: TommyDee</title>
    <description>The latest articles on DEV Community by TommyDee (@thomasdolso).</description>
    <link>https://dev.to/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>DEV Community: TommyDee</title>
      <link>https://dev.to/thomasdolso</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thomasdolso"/>
    <language>en</language>
    <item>
      <title>Build a scroll-driven WebGL hero in 30 lines</title>
      <dc:creator>TommyDee</dc:creator>
      <pubDate>Sat, 23 May 2026 15:24:31 +0000</pubDate>
      <link>https://dev.to/thomasdolso/build-a-scroll-driven-webgl-hero-in-30-lines-njo</link>
      <guid>https://dev.to/thomasdolso/build-a-scroll-driven-webgl-hero-in-30-lines-njo</guid>
      <description>&lt;p&gt;Hero sections that respond to scroll are one of those features that look complicated and actually aren't, once you have the right pieces. Two images, a shader that morphs between them, scroll position drives the morph. That's it. The rest is plumbing.&lt;/p&gt;

&lt;p&gt;This tutorial builds exactly that — a scroll-driven WebGL hero — in about 30 lines of JavaScript. Plain HTML, no framework, no build step. Drop it into a CodePen or a static HTML file and it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;Three behaviors, tied together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;As the user scrolls &lt;strong&gt;down into&lt;/strong&gt; the hero section, image A morphs into image B.&lt;/li&gt;
&lt;li&gt;While the hero is &lt;strong&gt;centered&lt;/strong&gt; in the viewport, the morph holds on image B.&lt;/li&gt;
&lt;li&gt;As the user scrolls &lt;strong&gt;down past&lt;/strong&gt; the hero, image B morphs back to image A.
That's the standard scroll-driven hero shape. Sites like Apple's product pages, Linear's marketing, and Vercel's case studies all use this pattern. The trick is the &lt;em&gt;holding&lt;/em&gt; part — without it, the transition feels like it ends too fast and the user never sees the destination.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The dependencies
&lt;/h2&gt;

&lt;p&gt;Two npm packages, both MIT, both tiny.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @vysmo/scroll @vysmo/transitions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined, that's about 6 KB gzipped. Less than a logo SVG.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTML
&lt;/h2&gt;

&lt;p&gt;A section, a canvas, two &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements that we'll use as transition sources, plus some content above and below to give the page something to scroll through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height: 200vh; padding-top: 50vh;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Above the hero&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Scroll down to see the effect.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hero"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height: 100vh; position: relative;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;canvas&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hero-canvas"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 100%; height: 100%;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/canvas&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"img-a"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/from.jpg"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display: none;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"img-b"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/to.jpg"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display: none;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height: 200vh;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Below the hero&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Keep scrolling.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements are &lt;code&gt;display: none&lt;/code&gt; because we're not rendering them as DOM — we're passing them to WebGL as texture sources. The browser still decodes them, which is what we need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JavaScript — 30 lines
&lt;/h2&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;crossZoom&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;createScrollTransition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scrollPlateau&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/scroll&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;section&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;HTMLElement&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;#hero&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;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;#hero-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="k"&gt;from&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;HTMLImageElement&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;#img-a&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;to&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;HTMLImageElement&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;#img-b&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="c1"&gt;// Wait for both images to decode before we use them as textures.&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="k"&gt;from&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;to&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="c1"&gt;// Match the canvas backing store to its CSS size so the shader renders sharp.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dpr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devicePixelRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;width&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="nx"&gt;clientWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dpr&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;height&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="nx"&gt;clientHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dpr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// One WebGL2 runner per canvas. It owns the compiled programs and FBOs.&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="c1"&gt;// Bind scroll progress to the transition.&lt;/span&gt;
&lt;span class="nf"&gt;createScrollTransition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;section&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="na"&gt;transition&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="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="c1"&gt;// Hold the "to" image while the section is 30%–70% through the viewport.&lt;/span&gt;
  &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;scrollPlateau&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="mf"&gt;0.7&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;That's the whole thing. Let me walk through the parts that aren't obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;scrollPlateau&lt;/code&gt; does
&lt;/h2&gt;

&lt;p&gt;This is the part of the API I'd most like to spend a paragraph on, because it's the small idea that makes the whole pattern work.&lt;/p&gt;

&lt;p&gt;Raw scroll progress is a 0 → 1 line as the section moves through the viewport. If you feed that directly into a transition, you get a morph that starts the moment the section enters and ends the moment it exits — which means the user only briefly sees the destination image as it flies past. Unsatisfying.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scrollPlateau(0.3, 0.7)&lt;/code&gt; reshapes that linear progress into a &lt;em&gt;bathtub curve&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0.0 → 0.3&lt;/strong&gt; of scroll progress: transition plays 0 → 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.3 → 0.7&lt;/strong&gt; of scroll progress: transition stays at 1 (the "hold")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.7 → 1.0&lt;/strong&gt; of scroll progress: transition plays 1 → 0
The result: the morph happens quickly as the section enters, the destination image gets to live on screen for the comfortable middle stretch, and the morph reverses on exit. Visually, the user feels like they "arrived" at something, instead of like something whisked past them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can change the plateau bounds to taste — &lt;code&gt;scrollPlateau(0.1, 0.9)&lt;/code&gt; makes the entry and exit transitions very fast, &lt;code&gt;scrollPlateau(0.4, 0.6)&lt;/code&gt; is slower and more deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swap the transition for a totally different feel
&lt;/h2&gt;

&lt;p&gt;The line &lt;code&gt;transition: crossZoom&lt;/code&gt; is the one that defines the &lt;em&gt;look&lt;/em&gt;. There are 60 built-in transitions, and switching is one identifier:&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;pageCurl&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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nf"&gt;createScrollTransition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;section&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="na"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pageCurl&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;ease&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now image A peels back like a page turn to reveal image B as you scroll. Same 30 lines of code, completely different aesthetic.&lt;/p&gt;

&lt;p&gt;A few transitions worth trying for a hero:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;crossZoom&lt;/code&gt; — the cinematic default; image A zooms in as it fades into B&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pageCurl&lt;/code&gt; — editorial, like turning a magazine page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;paintBleed&lt;/code&gt; — a paint-pour reveal, looks great for product launches&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;warpZoom&lt;/code&gt; — a chromatic warp; perfect for tech aesthetics&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;glassShatter&lt;/code&gt; — image A shatters into B; high-drama, use sparingly
Each is a one-line change to the import and the &lt;code&gt;transition&lt;/code&gt; prop. The &lt;a href="https://vysmo.com/transitions" rel="noopener noreferrer"&gt;catalog&lt;/a&gt; has all of them with live previews.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's happening under the hood
&lt;/h2&gt;

&lt;p&gt;In case the magic feels suspicious, here's what's actually running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;createScrollTransition&lt;/code&gt; registers your section with a shared &lt;code&gt;IntersectionObserver&lt;/code&gt; and a single &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop (one rAF for &lt;em&gt;all&lt;/em&gt; Vysmo scroll bindings on the page — that part matters for performance when you have multiple sections).&lt;/li&gt;
&lt;li&gt;On every animation frame where the section is intersecting the viewport, it computes a raw 0 → 1 progress value from the section's bounding-box position.&lt;/li&gt;
&lt;li&gt;It passes that value through the &lt;code&gt;ease&lt;/code&gt; function (&lt;code&gt;scrollPlateau&lt;/code&gt; in our case) to get a morphed progress.&lt;/li&gt;
&lt;li&gt;It calls &lt;code&gt;runner.render(transition, { from, to, progress })&lt;/code&gt;. The runner has the compiled WebGL2 shader cached, so this is just uniform updates and a draw call — sub-millisecond on a modern phone.
No internal timers, no playback state, no "play" / "pause" / "reverse" — the renderer is &lt;em&gt;idempotent&lt;/em&gt;. You give it a progress value, it draws that frame. Scroll up and the same code path runs the transition backwards. That's why the same library works equally well for rAF-driven animations, GSAP timelines, video exports, and scroll bindings.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Image must be decoded first.&lt;/strong&gt; If you skip the &lt;code&gt;await Promise.all([from.decode(), to.decode()])&lt;/code&gt; line, the first render might happen with one or both textures empty, and you'll see a flash of black. Always decode before passing to the Runner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canvas DPR.&lt;/strong&gt; Without the &lt;code&gt;canvas.width = canvas.clientWidth * dpr&lt;/code&gt; lines, the canvas backing store is whatever the browser defaults to (usually 300×150 px), which makes everything look like a pixelated mess. Match the backing store to the CSS size times device pixel ratio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't run on iOS Low Power Mode.&lt;/strong&gt; WebGL2 still works there but performance is throttled — the rAF callback may only fire 30 times per second instead of 60. Your transition will look choppy. Test on a real phone, not just your laptop with throttling disabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you'd want more
&lt;/h2&gt;

&lt;p&gt;This 30-line version covers a single hero. As your page gets more complex — multiple scroll-driven sections, scroll-driven text reveals layered on top, scroll-driven effects (a bloom that ramps as you enter, identity in the middle, fades on exit) — the same &lt;code&gt;@vysmo/scroll&lt;/code&gt; package has primitives for those too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;createScrollEffect&lt;/code&gt; — drives a &lt;code&gt;@vysmo/effects&lt;/code&gt; filter through the same three-zone envelope&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createScrollProgress&lt;/code&gt; — raw 0 → 1 emitter you can wire to anything (opacity, transform, your own state)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sharedScrollObserver&lt;/code&gt; — for when you're building your own scroll-driven renderer and want to plug into the same single-rAF batching
But you don't need any of that today. 30 lines, two images, one transition, one envelope. That's a shippable hero.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Full example, copy-pasteable, including the HTML scaffolding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;&lt;/span&gt;Vysmo Hero Demo&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"margin:0;font-family:system-ui;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height:200vh;padding-top:50vh;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"text-align:center;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Scroll down&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hero"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height:100vh;position:relative;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;canvas&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hero-canvas"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width:100%;height:100%;display:block;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/canvas&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"img-a"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/from.jpg"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"img-b"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/to.jpg"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height:200vh;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"text-align:center;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Keep scrolling&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;Runner&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://esm.sh/@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;createScrollTransition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scrollPlateau&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;https://esm.sh/@vysmo/scroll&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;section&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="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#hero&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="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#hero-canvas&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="k"&gt;from&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="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#img-a&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;to&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="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#img-b&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="k"&gt;from&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;to&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dpr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devicePixelRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;width&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="nx"&gt;clientWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dpr&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;height&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="nx"&gt;clientHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dpr&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="nf"&gt;createScrollTransition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;section&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="na"&gt;transition&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="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="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;scrollPlateau&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="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop two images named &lt;code&gt;from.jpg&lt;/code&gt; and &lt;code&gt;to.jpg&lt;/code&gt; next to that file, open it in a browser, and you have a working scroll-driven WebGL hero.&lt;/p&gt;

&lt;p&gt;The full catalog of transitions with live previews is at &lt;a href="https://vysmo.com/transitions" rel="noopener noreferrer"&gt;vysmo.com/transitions&lt;/a&gt;, and the scroll package docs are at &lt;a href="https://vysmo.com/scroll" rel="noopener noreferrer"&gt;vysmo.com/scroll&lt;/a&gt;. Source: &lt;a href="https://github.com/vysmodev/vysmo" rel="noopener noreferrer"&gt;github.com/vysmodev/vysmo&lt;/a&gt;. All MIT.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webgl</category>
    </item>
    <item>
      <title>Meet @vysmo/effects — 30 WebGL2 filter effects in one render() call</title>
      <dc:creator>TommyDee</dc:creator>
      <pubDate>Thu, 21 May 2026 16:18:10 +0000</pubDate>
      <link>https://dev.to/thomasdolso/meet-vysmoeffects-30-webgl2-filter-effects-in-one-render-call-2lng</link>
      <guid>https://dev.to/thomasdolso/meet-vysmoeffects-30-webgl2-filter-effects-in-one-render-call-2lng</guid>
      <description>&lt;p&gt;There's a category of visual work the browser is genuinely good at and yet most projects either skip or reinvent: putting a &lt;em&gt;filter&lt;/em&gt; on top of an image or a video. Blur a hero photo. Glow on hover. Halftone for the print-design aesthetic. ASCII for the terminal vibe. A scanline VHS pass for nostalgia.&lt;/p&gt;

&lt;p&gt;You can do these in WebGL, but the gap between "I want a bloom on this image" and "working ping-pong framebuffer pipeline with HDR float targets" is wide enough that most people give up and reach for a CSS &lt;code&gt;filter: blur(8px)&lt;/code&gt; and live with it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@vysmo/effects&lt;/code&gt; is the tour I would have wanted when I first looked at this. 30 effects, one API, ~9 KB for the whole thing. Multi-pass pipelines are handled for you.&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%2Fqevwe8ccuxbn8pfjjfob.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%2Fqevwe8ccuxbn8pfjjfob.png" alt="WebGL Effects" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Four lines
&lt;/h2&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;blur&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/effects&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;blur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&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;That's a real, shippable image filter — GPU-accelerated, runs at 60 fps on a phone. The &lt;code&gt;source&lt;/code&gt; can be an &lt;code&gt;HTMLImageElement&lt;/code&gt;, a canvas, a video, an ImageBitmap, an OffscreenCanvas, or ImageData. The Runner uploads it once and caches the compiled shader program between renders, so if you change &lt;code&gt;radius&lt;/code&gt; and re-render, you skip compilation entirely.&lt;/p&gt;

&lt;p&gt;Swap &lt;code&gt;blur&lt;/code&gt; for any of 29 others and you get a totally different look from the same five lines:&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="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;halftone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;dotSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&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;scanlines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;intensity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&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;ascii&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;charSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&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;oilPaint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;brushSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&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;swirl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;angle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.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;Each of those is one line and one new aesthetic. The catalog at &lt;a href="https://vysmo.com/effects" rel="noopener noreferrer"&gt;vysmo.com/effects&lt;/a&gt; has all 30 with a live playground — pick one, drop your own image, tune the params.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this gets technically interesting: multi-pass HDR
&lt;/h2&gt;

&lt;p&gt;Most filter effects are a single fragment shader pass: sample the image, do some math, write the output. Easy. The Runner draws once per &lt;code&gt;render()&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;But some effects can't be done that way. &lt;strong&gt;Bloom&lt;/strong&gt; is the canonical example. To make highlights glow believably, you need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;bright-pass&lt;/strong&gt; filter that isolates pixels above a luminance threshold&lt;/li&gt;
&lt;li&gt;Several passes of a &lt;strong&gt;separable Gaussian blur&lt;/strong&gt; on those highlights (horizontal, then vertical — multiple radii)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;composite&lt;/strong&gt; pass that adds the blurred highlights back over the original
Four passes, minimum. And to do it correctly, those intermediate textures need to be &lt;strong&gt;HDR floating-point&lt;/strong&gt; (&lt;code&gt;RGBA16F&lt;/code&gt;) — because once you've isolated highlights at 2× or 3× max brightness, an 8-bit-per-channel buffer clamps them to 1.0 and your bloom looks flat and ugly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;code&gt;@vysmo/effects&lt;/code&gt;, this is the API:&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="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;bloom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;intensity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&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;Same shape as the blur example. One line. Internally the Runner allocates ping-pong &lt;code&gt;RGBA16F&lt;/code&gt; framebuffers when the GPU supports them (every WebGL2 device built since 2017 does), runs the four-pass pipeline, and disposes the intermediate targets when you call &lt;code&gt;runner.dispose()&lt;/code&gt;. You never see any of this.&lt;/p&gt;

&lt;p&gt;The same architecture handles &lt;strong&gt;glow&lt;/strong&gt; (similar multi-pass with a different bright-pass response curve), &lt;strong&gt;motion blur&lt;/strong&gt; (multi-tap directional sampling), and &lt;strong&gt;datamosh&lt;/strong&gt; (frame-history accumulation).&lt;/p&gt;

&lt;p&gt;If you've ever tried to ship bloom from scratch with vanilla WebGL, you know how much code that hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30 effects, roughly grouped
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Image cleanup and adjustment&lt;/strong&gt; — &lt;code&gt;blur&lt;/code&gt;, &lt;code&gt;sharpen&lt;/code&gt;, &lt;code&gt;color-grade&lt;/code&gt;, &lt;code&gt;tint&lt;/code&gt;, &lt;code&gt;dither&lt;/code&gt;, &lt;code&gt;gradient-map&lt;/code&gt;. The everyday tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Print and editorial aesthetics&lt;/strong&gt; — &lt;code&gt;halftone&lt;/code&gt;, &lt;code&gt;oil-paint&lt;/code&gt;, &lt;code&gt;ascii&lt;/code&gt;. The looks designers reach for to make web feel like print.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cinematic and lens&lt;/strong&gt; — &lt;code&gt;bloom&lt;/code&gt;, &lt;code&gt;glow&lt;/code&gt;, &lt;code&gt;tilt-shift&lt;/code&gt;, &lt;code&gt;lens-distortion&lt;/code&gt;, &lt;code&gt;motion-blur&lt;/code&gt;. The looks that make web feel like film.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retro and degraded&lt;/strong&gt; — &lt;code&gt;scanlines&lt;/code&gt;, &lt;code&gt;vhs&lt;/code&gt;, &lt;code&gt;datamosh&lt;/code&gt;. The looks people use when nostalgia is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distortion and warp&lt;/strong&gt; — &lt;code&gt;wave&lt;/code&gt;, &lt;code&gt;swirl&lt;/code&gt;. The looks for headers that want to feel alive.&lt;/p&gt;

&lt;p&gt;Plus another dozen I'm not going to enumerate because that's what the catalog page is for.&lt;/p&gt;

&lt;p&gt;A single-pass effect costs you a few hundred bytes in your bundle. A multi-pass effect costs you maybe a kilobyte. Import the two or three you actually use and your effects bundle is well under 2 KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authoring your own
&lt;/h2&gt;

&lt;p&gt;Same &lt;code&gt;defineX&lt;/code&gt; pattern as transitions. Write a fragment shader that exports &lt;code&gt;vec4 effect(vec2 uv)&lt;/code&gt;, declare your params, done:&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;defineEffect&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vysmo/effects&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;sepia&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineEffect&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;sepia&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;strength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&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 uStrength;
    vec4 effect(vec2 uv) {
      vec4 src = getSource(uv);
      float gray = dot(src.rgb, vec3(0.299, 0.587, 0.114));
      vec3 sepia = vec3(gray * 1.2, gray * 1.0, gray * 0.8);
      return vec4(mix(src.rgb, sepia, uStrength), src.a);
    }
  `&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;sepia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;strength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&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 Runner allocates FBOs as needed, manages uniforms, and dispatches passes — you write the math. The &lt;code&gt;defaults&lt;/code&gt; object also drives type inference, so &lt;code&gt;params&lt;/code&gt; is fully typed in your editor without you writing &lt;code&gt;Effect&amp;lt;{ strength: number }&amp;gt;&lt;/code&gt; anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it composes
&lt;/h2&gt;

&lt;p&gt;Two patterns that show up constantly in real projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effects on video.&lt;/strong&gt; Pass a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element as &lt;code&gt;source&lt;/code&gt; and re-render every frame:&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="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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;frame&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;bloom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&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;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;intensity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Effects driven by interaction.&lt;/strong&gt; Hook the params object up to a slider, scroll progress, or mouse position. The Runner caches the compiled program, so re-rendering with different params is essentially free:&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="nx"&gt;slider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;blur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&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;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's an interactive blur control in eight lines of code, with a draw call that costs less than half a millisecond on a modern phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @vysmo/effects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then four lines:&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;bloom&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/effects&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;image&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="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;img&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;image&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;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;bloom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="https://vysmo.com/effects" rel="noopener noreferrer"&gt;vysmo.com/effects&lt;/a&gt; for the catalog and the playground where you can drop your own image and try every effect at every parameter combination. Source and issue tracker at &lt;a href="https://github.com/vysmodev/vysmo" rel="noopener noreferrer"&gt;github.com/vysmodev/vysmo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All Vysmo libraries are MIT, zero dependency, free forever. There's no commercial tier and no telemetry.&lt;/p&gt;

</description>
      <category>webgl</category>
      <category>javascript</category>
      <category>animation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Meet @vysmo/text - 243 text animation presets in 3 KB</title>
      <dc:creator>TommyDee</dc:creator>
      <pubDate>Tue, 19 May 2026 21:50:11 +0000</pubDate>
      <link>https://dev.to/thomasdolso/meet-vysmotext-243-text-animation-presets-in-3-kb-2318</link>
      <guid>https://dev.to/thomasdolso/meet-vysmotext-243-text-animation-presets-in-3-kb-2318</guid>
      <description>&lt;p&gt;The big text on a landing page deserves to &lt;em&gt;arrive&lt;/em&gt;. The headline you spent two hours writing shouldn't just appear — it should choreograph itself into place, letter by letter, so the reader's eye lands on it and stays.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;@vysmo/text&lt;/code&gt; does. It's a new addition to the &lt;a href="https://vysmo.com" rel="noopener noreferrer"&gt;vysmo libraries&lt;/a&gt; — a tiny, tree-shakable, zero-dependency text animation library with 243 presets and one of the cleanest APIs I've used.&lt;/p&gt;

&lt;p&gt;This is a tour.&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%2Fhahvhz8i8087ptfrq4dd.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%2Fhahvhz8i8087ptfrq4dd.png" alt="Text Animations" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three lines
&lt;/h2&gt;

&lt;p&gt;The simplest possible use:&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;animateText&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/text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/fade-up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real, shippable hero animation. The library splits the text into grapheme-safe slices (more on that below), applies the preset's choreography, and starts playing on mount. No timeline setup, no &lt;code&gt;.from()&lt;/code&gt; / &lt;code&gt;.to()&lt;/code&gt; chains, no ref management.&lt;/p&gt;

&lt;p&gt;If you'd rather have a different feel, change the preset name:&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="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/elastic-rise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;   &lt;span class="c1"&gt;// soft spring&lt;/span&gt;
&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/bloom-scatter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// letters arrive from depth&lt;/span&gt;
&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/whirl-scatter-curl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// dramatic, full-on&lt;/span&gt;
&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/blur-in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;        &lt;span class="c1"&gt;// tasteful, agency-deck-ready&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same one line. 120 enter presets to choose from. The catalog at &lt;a href="https://vysmo.com/text" rel="noopener noreferrer"&gt;vysmo.com/text&lt;/a&gt; has all of them with a live playground — click one, type your text, see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  243 presets, 3 KB
&lt;/h2&gt;

&lt;p&gt;The headline number is real: &lt;strong&gt;229 generated + 14 curated = 243 presets&lt;/strong&gt;, in roughly 3 KB gzipped. That's a smaller bundle than most loading spinners.&lt;/p&gt;

&lt;p&gt;The trick is tree-shaking. Each preset is its own ES module export. You import what you ship:&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;animateText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fadeUp&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/text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fadeUp&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// pass the object, not the string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you pass the preset by &lt;em&gt;reference&lt;/em&gt;, only that preset's data lands in your bundle. The other 242 stay tree-shaken out. The string-name form (&lt;code&gt;"enter/fade-up"&lt;/code&gt;) pulls in a tiny registry — pick whichever ergonomics you prefer.&lt;/p&gt;

&lt;p&gt;For a typical landing page using two or three presets, you ship under 2 KB total. For a kinetic-typography portfolio site that uses twenty different presets, you ship maybe 4 KB. Either way, less than a logo SVG.&lt;/p&gt;

&lt;h2&gt;
  
  
  The presets are split into three categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Enter (120)&lt;/strong&gt; — animations for text arriving. &lt;code&gt;fade-up&lt;/code&gt;, &lt;code&gt;elastic-rise&lt;/code&gt;, &lt;code&gt;bloom-scatter&lt;/code&gt;, &lt;code&gt;flip-up-spring&lt;/code&gt;, &lt;code&gt;tunnel&lt;/code&gt;, plus 115 more. Tuned to feel deliberate, not gimmicky. Most run for ~600–900 ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exit (92)&lt;/strong&gt; — animations for text leaving. The conceptual mirror of enter, but harder to design well — exits need to feel intentional, not glitchy. &lt;code&gt;fade-down&lt;/code&gt;, &lt;code&gt;mist-out&lt;/code&gt;, &lt;code&gt;collapse-burst&lt;/code&gt;, &lt;code&gt;pinwheel-out&lt;/code&gt;. Most react well to being driven faster than their default duration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Emphasis (31)&lt;/strong&gt; — animations that loop or play on demand to draw attention. &lt;code&gt;pulse&lt;/code&gt;, &lt;code&gt;shake&lt;/code&gt;, &lt;code&gt;wobble&lt;/code&gt;, &lt;code&gt;coin-flip&lt;/code&gt;, &lt;code&gt;spin&lt;/code&gt;. These are what you wire to a button click or a "10 items left" badge.&lt;/p&gt;

&lt;p&gt;A preset is just data:&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;fadeUp&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/text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fadeUp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   name: "enter/fade-up",&lt;/span&gt;
&lt;span class="c1"&gt;//   split: "character",&lt;/span&gt;
&lt;span class="c1"&gt;//   stagger: 30,&lt;/span&gt;
&lt;span class="c1"&gt;//   animations: [&lt;/span&gt;
&lt;span class="c1"&gt;//     { prop: "opacity",    from: 0,  to: 1, duration: 500, ease: power2.out },&lt;/span&gt;
&lt;span class="c1"&gt;//     { prop: "translateY", from: 20, to: 0, duration: 500, ease: power2.out },&lt;/span&gt;
&lt;span class="c1"&gt;//   ],&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No classes. No inheritance. No registry magic. You can &lt;code&gt;console.log&lt;/code&gt; a preset and read it. You can copy one and modify it. You can author your own with the exact same shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two knobs that change everything
&lt;/h2&gt;

&lt;p&gt;Every preset has a default — but the API lets you override any knob per call. Two of them, in particular, are the cheapest way to make the same preset feel completely different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;stagger&lt;/code&gt;&lt;/strong&gt; is the milliseconds between consecutive slices starting. The default is 30 ms (energetic, fast). Bump it to 80 ms and the same preset feels slow and contemplative. Drop it to 10 ms and slices arrive almost together — more of a wash than a choreography.&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="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/fade-up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stagger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// thoughtful&lt;/span&gt;
&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/fade-up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stagger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// urgent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;staggerOrder&lt;/code&gt;&lt;/strong&gt; is &lt;em&gt;who&lt;/em&gt; gets staggered first. Default is &lt;code&gt;"start"&lt;/code&gt; (left-to-right, like reading). But:&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="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/fade-up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;staggerOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;    &lt;span class="c1"&gt;// right-to-left&lt;/span&gt;
&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/fade-up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;staggerOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// middle outward&lt;/span&gt;
&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/fade-up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;staggerOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edges&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// both ends inward&lt;/span&gt;
&lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/fade-up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;staggerOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;random&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// chaos mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"center"&lt;/code&gt; is the secret weapon. Try it on a hero headline — letters bloom outward from the middle. It feels like the title is being projected rather than typed. Most libraries don't expose this and people don't realize how much they're missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scrolling drives it for free
&lt;/h2&gt;

&lt;p&gt;The handle that &lt;code&gt;animateText&lt;/code&gt; returns has a &lt;code&gt;.seek(progress)&lt;/code&gt; method that takes a value between 0 and 1 and snaps the animation to that point in its timeline. That's the only thing you need to drive a text animation from scroll position:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/bloom-scatter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoPlay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// In a scroll handler — pair with @vysmo/scroll, ScrollTrigger, or your own IntersectionObserver:&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onScroll&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cinematic word-by-word reveals tied to scroll position. Two lines. The reason this works cleanly is the library's single-master-clock architecture — every slice is animated against the same time origin, so seeking is coherent. That's worth a post on its own (it's coming).&lt;/p&gt;

&lt;h2&gt;
  
  
  The split is grapheme-safe
&lt;/h2&gt;

&lt;p&gt;One detail that separates this library from most of its predecessors: the text splitting uses &lt;code&gt;Intl.Segmenter&lt;/code&gt;. Which means it correctly handles emoji clusters, regional flags, combining marks, and connected scripts.&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;// Most libraries:&lt;/span&gt;
&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;👨‍👩‍👧&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="c1"&gt;// ['👨','‍','👩','‍','👧']  — family emoji shattered into 5 broken pieces&lt;/span&gt;

&lt;span class="c1"&gt;// @vysmo/text:&lt;/span&gt;
&lt;span class="nf"&gt;splitText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;👨‍👩‍👧&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;character&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;slices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 1 — correct&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your animation has ever broken for users in India, Japan, the UAE, or anywhere with an emoji-rich messaging culture, this is why. The fix has been in browsers since 2020 and most libraries still don't use it. Vysmo does, by default.&lt;/p&gt;

&lt;p&gt;Word-mode and line-mode use the same Segmenter, which means they also work correctly in Thai (no spaces between words), Japanese (mixed scripts), and Arabic (right-to-left). If you ship to a global audience by default — which, in 2026, is everyone — this matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  React, too
&lt;/h2&gt;

&lt;p&gt;There's a thin companion package, &lt;code&gt;@vysmo/text-react&lt;/code&gt;, that wraps the runtime in a declarative component plus a &lt;code&gt;useAnimateText&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;AnimateText&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/text-react&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;function&lt;/span&gt; &lt;span class="nf"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnimateText&lt;/span&gt; &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h1"&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"enter/fade-up"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Hello world
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AnimateText&lt;/span&gt;&lt;span class="p"&gt;&amp;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;That's the whole component. Props pass through to the underlying runtime — &lt;code&gt;preset&lt;/code&gt;, &lt;code&gt;split&lt;/code&gt;, &lt;code&gt;stagger&lt;/code&gt;, &lt;code&gt;repeat&lt;/code&gt;, all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @vysmo/text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then three lines:&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;animateText&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/text&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;h1&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="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;h1&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="nf"&gt;animateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter/bloom-scatter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="https://vysmo.com/text" rel="noopener noreferrer"&gt;vysmo.com/text&lt;/a&gt; for the live catalog and the studio that lets you tweak every knob. Source and issue tracker at &lt;a href="https://github.com/vysmodev/vysmo" rel="noopener noreferrer"&gt;github.com/vysmodev/vysmo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Vysmo libraries are MIT, free, and zero-dependency — there's no commercial tier, no rate limit, no telemetry. Just one library doing one thing as cleanly as I could get it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>animation</category>
      <category>typescript</category>
    </item>
    <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://dev.to/thomasdolso/migrating-from-gl-transitions-to-vysmotransitions-the-diff-that-matters-g82</link>
      <guid>https://dev.to/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://dev.to/thomasdolso/i-just-open-sourced-13-mit-libraries-for-web-visual-effects-2g86</link>
      <guid>https://dev.to/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>
