WebGL enhanced drag slider tutorial with curtains.js (part 3)
Overview
Please note that this tutorial was originally posted on Medium here.
This article is the third and last part of our tutorial. In the first part, we’ve created a javascript drag slider. If you haven’t read it yet, go check it here. In the second part, we've added WebGL thanks to curtains.js to improve the animations. If you haven’t read it yet, go check it here.
In this final part, we'll see how to improve the overall performance of the slider by removing any layout repaint calls. Here's the final pen:
As a reminder, we used curtains.js to add everything related to WebGL. curtains.js is an open source vanilla javascript library. It is particularly useful here as its main purpose is to enhance DOM elements with WebGL effects. With few lines of javascript, you were able to create WebGL textured planes bound to our slider items HTML elements and then post-process the whole scene.
We can then easily see the advantages of using curtains.js:
Clean & SEO friendly HTML code.
You don’t have to worry about your WebGL objects sizes and positions as most of the stuff (like resize) will be handled under the hood by the library.
If in any case there’s an error during the WebGL initialization or in your shaders, the slider will still work!
Part 1. Layout repaint calls / reflow
Overall concept
First of all, you'll need to understand what are layout repaint / reflow and what causes them. There's a neat article wrote by Paul Irish explaining it on GitHub.
Basically, every time you're using a javascript method to get an HTML element style or position like element.getComputedStyle() or element.getBoundingClientRect(), or every time you're trying to access the window scroll value via window.scrollY, the browser needs to redraw the whole web page to calculate those values.
This could lead to performance bottleneck and cause jank issues.
Using curtains.js
As a matter of fact, curtains.js does internally use getBoundingClientRect each time you're adding a plane or each time you resize your window to copy the plane HTML size and position and apply it to its associated WebGL plane object. There's no way to avoid that but fortunately this does not happen often so that's not really a big deal.
But we used the updatePosition method in our slider onTranslation hook, and this function also calls getBoundingClientRect, therefore triggering a reflow call. We'll now see how to improve our slider to get rid of those layout repaint calculations.
Part 2. Another way to translate our planes
Using setRelativePosition method
The library provides another way to translate a plane: the setRelativePosition function. It takes an X and an Y value in pixels as parameters and apply it to your plane by updating its model view matrix.
Usecases
Usually when using curtains.js, you'll be using the updatePosition inside a scroll event listener to update the positions of your planes according to the window scroll value. Because there's no way to get your scroll or HTML element position without triggering a reflow call, that's still the best way to handle planes positions when using a native scroll.
But if you are using a Virtual Scroll library for example, you'll have access to scroll values that gets calculated without causing reflow calls and you'll be able to update your planes positions accordingly with our setRelativePosition method!
Now, back to our slider. At any given time, we have access to its current translation (that gets calculated without triggering reflow) so why not using it to update our positions instead? That's precisely what we are going to do.
The idea
The overall concept is fairly simple: we will apply our translation to the planes using the setRelativePosition function.
Since each time we'll be resizing our browser window, our planes positions will be automatically updated,** we'll have to reset their relative positions** there. By doing that, we'll break the sync between the slider current translation and our relative positions values. We then have to keep track of our slider previous translation and subtract thoses values to our relative position: the slider and the WebGL are now synced again!
Updating the code
First of all, we'll add a previousTranslation property to our WebGL slider class. Let's update our WebGLSlider class constructor:
class WebGLSlider extends Slider {
/*** CONSTRUCTOR ***/
constructor(options) {
super(options);
// tweening
this.animation = null;
// value from 0 to 1 to pass as uniform to the WebGL
// will be tweened on mousedown / touchstart and mouseup / touchend events
this.effect = 0;
// our WebGL variables
this.curtains = null;
this.planes = [];
// we will keep track of the previous translation values on resize
this.previousTranslation = {
x: 0,
y: 0,
};
this.shaderPass = null;
// set up the WebGL part
this.setupWebGL();
}
/*** WEBGL INIT ***/
...
/*** PLANES CREATION ***/
...
/*** SHADER PASS CREATION ***/
...
/*** HELPER ***/
...
/*** HOOKS ***/
...
/*** DESTROY ***/
...
}
Next we'll have to update our onTranslation hook to use our setRelativePosition function instead of the previous updatePosition one:
class WebGLSlider extends Slider {
/*** CONSTRUCTOR ***/
...
/*** WEBGL INIT ***/
...
/*** PLANES CREATION ***/
...
/*** SHADER PASS CREATION ***/
...
/*** HELPER ***/
...
/*** HOOKS ***/
// ... previous hooks here
// this is called continuously while the slider is translating
onTranslation() {
// get our slider translation and take our previous translation into account
var planeTranslation = {
x: this.direction === 0 ? this.translation - this.previousTranslation.x : 0,
y: this.direction === 1 ? this.translation - this.previousTranslation.y : 0,
};
// keep our WebGL planes position in sync with their HTML elements
for(var i = 0; i < this.planes.length; i++) {
// in the previous CodePen we were using updatePosition the method which handles positioning automatically
// however this method internally calls getBoundingClientRect() which causes a reflow and therefore impacts performance
// so we will position our planes manually with setRelativePosition instead, which does not trigger a layout repaint call
this.planes[i].setRelativePosition(planeTranslation.x, planeTranslation.y);
}
// shader pass displacement texture offset
if(this.shaderPass) {
// we will offset our displacement effect on main axis so it follows the drag
var offset = ((this.direction - 1) * 2 + 1) * this.translation / this.boundaries.referentSize;
this.shaderPass.uniforms.offset.value[this.direction] = offset;
}
}
// ... next hooks there
/*** DESTROY ***/
...
}
The last thing we need to do is rewrite our onSliderResized hook to update our previousTranslation property values and reset our planes relative positions, and we're done!
class WebGLSlider extends Slider {
/*** CONSTRUCTOR ***/
...
/*** WEBGL INIT ***/
...
/*** PLANES CREATION ***/
...
/*** SHADER PASS CREATION ***/
...
/*** HELPER ***/
...
/*** HOOKS ***/
// ... previous hooks here
// this is called after our slider has been resized
onSliderResized() {
// we need to update our previous translation
this.previousTranslation = {
x: this.direction === 0 ? this.translation : 0,
y: this.direction === 1 ? this.translation : 0,
};
// reset our slides relative positions
// because during the resize their positions has already been updated internally
for(var i = 0; i < this.planes.length; i++) {
this.planes[i].setRelativePosition(0, 0);
}
// update our direction uniform
if(this.shaderPass) {
// update direction
this.shaderPass.uniforms.direction.value = this.direction;
}
}
/*** DESTROY ***/
...
}
And that's all folks! Now you're ready to use curtains.js on your website with a minimum performance impact.
You can try to change some values in your shader pass fragment shaders to tweak the effect a bit. You can also try to use a different displacement image (remember you’ll have to use a pattern to obtain a seamless effect).
Here’s a bonus example with a different displacement image and a few variations in the shaders:
Conclusion
I hope you liked this article and found it useful. I also hope you saw how easy it is to use curtains.js to enhance your UI with neat WebGL effects. Have a look at the library’s documentation and examples or go check its GitHub repo if you want to know more.
Be creative!