Hi 👋 My name is Amagi, a freelance frontend developer based in Vancouver.
Recently I released a library named VFX-JS, which allows you to add fancy visual effects to your projects. With VFX-JS, you can achieve advanced visual effects like these, without the hassle of dealing with WebGL:
In this article, I’ll explain why I created VFX-JS, how it works, and how you can take your visuals to the next level.
What is VFX-JS?
VFX-JS is a JavaScript library that makes it easy to add WebGL-powered effects to DOM elements, like images and videos.
Creating these kinds of visual effects with WebGL usually involves a lot of tedious setup. While libraries like Three.js can help, you still need to configure cameras, geometry, materials, and textures—even for something as simple as displaying a single image.
VFX-JS changes that. It allows you to use graphics elements, like images or videos, just as you would when designing a standard website with HTML and CSS. VFX-JS automatically generates 3D objects and applies visual effects, so you can focus on creating stunning visuals without worrying about the technical setup.
To use VFX-JS in your project, just install it from npm:
npm i @vfx-js/core
Then create a VFX object in your script, do the following:
import { VFX } from '@vfx-js/core';
const img = document.querySelector('#img');
const vfx = new VFX();
vfx.add(img, { shader: "glitch", overflow: 100 });
…and BOOM!! You’ll see this glitch effect💥
Custom Shaders
VFX-JS includes several preset effects, but you can also create your own by writing a GLSL shader.
A shader is the backbone of WebGL visual effects. It’s essentially a small program that runs in WebGL for every object on the screen, determining the final color output of a 3D object. Shaders offer incredible flexibility—you can perform color correction, glitch effects, parallax animations, or even create entirely new 3D scenes.
Rather than using a preset effect, you can pass a custom fragment shader to the shader
parameter:
const shader = `
precision highp float;
uniform vec2 resolution;
uniform vec2 offset;
uniform sampler2D src;
out vec4 outColor;
void main (void) {
outColor = texture2D(src, (gl_FragCoord.xy - offset) / resolution);
outColor.rgb = 1. - outColor.rgb; // Invert colors!
}
`;
vfx.add(el, { shader });
By writing custom shaders, you can easily create effects that respond to user interactions and more. We’ll explore this in greater detail later in the article.
Use VFX-JS in CodePen
VFX-JS is available on CDNs like esm.sh or jsDeliver. You can use it in CodePen with just a single line of code!
import { VFX } from "https://esm.sh/@vfx-js/core";
This is very useful for quickly sketching out your ideas.
See the Pen
VFX-JS scroll animation by Amagi (@fand)
on CodePen.
Why WebGL?
VFX-JS works by automatically loading the specified element as a WebGL texture and applying shader effects to it. But what exactly is WebGL?
Imagine you want to animate a single image on your website. You could move the image using CSS with a @keyframes
animation or a property like transition: left 1s;
.
You could also use JavaScript to manually change the image’s properties.
However, you’ll notice that all these solutions only give us control over DOM properties. They don’t allow us to pixelate an image, apply color correction, add a VHS effect, or create similar advanced effects.
This is where WebGL comes into play.
WebGL is a set of 3D graphics APIs for the web (essentially, OpenGL for the web). It’s widely used in websites, games, physics simulations, and AI. Modern PCs and smartphones come equipped with a GPU (Graphics Processing Unit), designed specifically for graphics tasks. WebGL leverages the GPU, providing immense computational power that can be used to create stunning visual effects.
So yes, with WebGL, you can achieve effects like pixelation and much more!
In fact, if you’re aiming to create advanced-level visuals on the web, using WebGL is almost unavoidable. You can explore many articles about WebGL on Codrops: https://tympanus.net/codrops/tag/webgl
How VFX-JS Works
VFX-JS handles all the tedious aspects of WebGL, but its underlying mechanics are straightforward. It overlays the entire window with a large WebGL canvas and positions 3D elements to match the original locations of your images and videos.
- Load
<img>
/<video>
as a WebGL texture - Add 3D planes to match the positions of the elements
- Synchronize with the original
<img>
or<video>
- Apply shader effects to enhance visuals
Here’s an illustration of how it sets up 3D elements:
Text Effects
VFX-JS has another unique feature: it can apply visual effects to text elements, like <span>
and <h1>
. Creating text effects in WebGL is usually quite difficult and tedious. Most WebGL libraries don’t support text rendering out of the box, and even if they do, you have to manually create 3D text geometries or use sprite textures, which come with limited styling options.
VFX-JS solves this with some black magic under the hood 🔮. It converts text elements into SVG images using an SVG feature you’ve probably never heard of (foreignObject
), draws them onto a canvas, and then loads them as WebGL textures. Yeah, it’s kinda terrifying, but it works:
Though it’s still somewhat experimental and has a few limitations (e.g., deeply nested elements), it’s incredibly useful for sketching text effects for your website.
Case Study: Pixelation Effect
In this chapter, we’re going to learn how to write custom effects using VFX-JS.
We’ll be using a new language called GLSL… but trust me, it’s easier than it sounds!
Let’s start with a simple pixelation effect.
It’s straightforward: we’ll take an input as a WebGL texture and display it on the screen.
You can try it out on CodePen:
See the Pen
VFX-JS tutorial: Pixelation Effect by Amagi (@fand)
on CodePen.
First, we set up a VFX object and pass in the input element:
import { VFX } from '@vfx-js/core';
// 1. Get the image element
const img = document.querySelector('#img');
// 2. Register the image to VFX-JS
const vfx = new VFX();
vfx.add(img, { shader: "rainbow" });
Then let’s create a custom shader!
const shader = `
uniform vec2 resolution;
uniform vec2 offset;
uniform sampler2D src;
out vec4 outColor;
void main() {
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
vec4 color = texture(src, uv);
outColor = color;
}
`;
const vfx = new VFX();
vfx.add(img, { shader });
Congratulations! 🎉 This is your first GLSL shader. In this code, you’re doing the following:
- Calculate the UV coordinate from the pixel position (
gl_FragCoord.xy
). - Extract the color from the input texture.
- Assign the color to the output (
outColor
).
// Calculate the UV coordinate
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
// Read the input texture
vec4 color = texture(src, uv);
// Assign the color to the output
outColor = color;
The UV coordinate represents the XY position used to fetch the texture color. Normally, uv
values are within the range of 0 to 1: the bottom-left corner corresponds to vec2(0.0, 0.0)
, and the top-right corner corresponds to vec2(1.0, 1.0)
.
Now you can tweak the code to adjust colors, animate positions, add glitches, or anything else you can imagine! For instance, you can invert the RGB values with just one line of code:
vec4 color = texture2D(src, uv);
+ color.rgb = 1. - color.rgb; // Invert!
outColor = color;
This time, let’s modify the uv
to create a pixelation effect. Add the following line of code:
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
+ uv.x = floor(uv.x * 10.0) / 10.0;
vec4 color = texture(src, uv);
floor()
is a function that rounds a number down to the nearest integer. When used with uv
values, it creates a stair-step pattern by turning smooth gradients into discrete steps.
Et voilà ! We’ve created a pixelation effect with just a single line of code!
You can edit the shader and experiment with different variations.
Additionally, VFX-JS includes a built-in time
variable that tracks the number of seconds since VFX-JS started. You can use this to animate your effects dynamically!
In this example, I adjusted the pixelation level, replaced uv
with uv.x
(to pixelate only the X coordinate), and animated it using the time
variable. I highly recommend experimenting with the code yourself—it’s a great way to understand how shaders work in VFX-JS and WebGL!
Case Study 2: Mouse Shift Effect
One of the great things about VFX-JS is that you can dynamically connect effects to JavaScript!
Let’s create a scroll effect like this and learn how to link shaders to JavaScript values.
See the Pen
VFX-JS tutorial: Mouse Shift by Amagi (@fand)
on CodePen.
Once again, let’s start with a simple fragment shader:
uniform vec2 resolution;
uniform vec2 offset;
uniform sampler2D src;
out vec4 outColor;
void main() {
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
outColor = texture(src, uv);
}
We’re going to track mouse movement. Let’s add an event listener for pointermove
events.
let pos = [0.5, 0.5]; // Current mouse position
let posDelay = [0.5, 0.5]; // Delayed mouse position
// Update target on mouse move
window.addEventListener('pointermove', (e) => {
const x = e.clientX / window.innerWidth;
const y = e.clientY / window.innerHeight;
pos = [x, y];
});
Here, pos
and posDelay
move within the range of [0, 1], so their default value is set to 0.5.
Next, let’s calculate the mouse velocity and pass it to the shader:
// Linear interpolation
const mix = (a, b, t) => a * (1 - t) + b * t;
vfx.add(document.getElementById('img'), {
shader,
uniforms: {
velocity: () => {
// Move posDelay toward the mouse position
posDelay = [
mix(posDelay[0], pos[0], 0.05),
mix(posDelay[1], pos[1], 0.05),
];
// Return the diff as velocity
return [
pos[0] - posDelay[0],
pos[1] - posDelay[1],
];
}
},
});
I added a helper function called mix
for better readability. This function performs linear interpolation between the arguments a
and b
using the factor t
. For example, mix(0, 1, 0.5)
gives 0.5, mix(0, 1, 0.1)
gives 0.1, and mix(0, 100, 0.1)
gives 10, and so on. Linear interpolation is a common technique in graphics programming, so it’s worth learning if you’re not already familiar with it.
In this code, we defined a function that calculates a uniform variable named velocity
. This function executes every frame (≈ 60Hz) and moves posDelay
smoothly toward pos
, which represents the current position of the mouse pointer. The function then returns the difference between pos
and posDelay
, effectively giving us the velocity of the mouse pointer.
Next, we’ll use the velocity
in the shader code:
uniform vec2 velocity;
void main() {
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
// Shift texture position
uv -= velocity * vec2(1, -1); // flip Y
outColor = texture(src, uv);
}
Now, you’ve made the image respond to the mouse movement!
Finally, let’s make it a bit more dynamic with the RGB shift technique by modifying the position shift value for each color channel:
vec2 d = velocity * vec2(-1, 1);
// Shift the position for each channel
vec4 cr = texture(src, uv + d * 1.0);
vec4 cg = texture(src, uv + d * 1.5);
vec4 cb = texture(src, uv + d * 2.0);
// Composite
outColor = vec4(cr.r, cg.g, cb.b, cr.a + cg.a + cb.a);
And boom!💥
Want to Learn More About GLSL?
There are plenty of tutorials and learning materials available online. For video tutorials, I recommend checking out tutorials by the artist Kishimisu and the YouTube channel The Art of Code.
If you enjoy reading, The Book of Shaders is my go-to reference for beginners.
Future of VFX-JS
I developed VFX-JS to empower developers and designers to create WebGL-powered visual effects without getting bogged down in the technical setup. While I believe VFX-JS fulfills this purpose well, there are exciting challenges ahead that I’m eager to tackle.
One major missing feature in VFX-JS is a plugin system. Although it already offers powerful custom shader capabilities, wiring shaders and passing uniform variables can feel tedious. Wouldn’t it be great if we could package effects, like a Lens Distortion effect, as ES modules and share them online for others to use?
I plan to implement a plugin system that allows us to package effects with their internal state management while exposing only user-defined parameters. This means effects could be reused across websites with just a few parameter changes and even shared through GitHub or npm.
Another key focus for VFX-JS is stability. While it performs well in production, there are known performance issues, particularly with scrolling lag. We have several promising solutions in the works, and I’m confident we’ll resolve these challenges soon.
That wraps up this article. I hope you’ll explore the world of shaders and create some mind-blowing visuals with VFX-JS!
If you have any questions, feel free to send me a message 👋