WebGL enhanced drag slider tutorial with curtains.js (part 2)
Overview
Please note that this tutorial was originally posted on Medium here.
This article is the second 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.
We are now going to write the WebGL part. Here’s what you’ll end up with:
As a reminder, we’ll be using curtains.js to add everything related to WebGL. curtains.js is an open source vanilla javascript library. It will be particularly useful here as its main purpose is to enhance DOM elements with WebGL effects. With few lines of javascript, you will be 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!
Even tho we’re going to see how it works step by step with a fully commented code, you may want to learn a bit more about WebGL and shaders if you‘re unfamiliar with those concepts.
You may also want to look at the curtains.js API documentation or check its GitHub repo.
We’re also going to use anime.js as a tweening engine for our animations.
Part 1. HTML & CSS
In order to implement the WebGL we’ll have to add a few modifications to the HTML and the CSS.
HTML
There’s not many changes in the HTML. We will add a div container that will hold our WebGL canvas and data-sampler attribute on the image tags: it will be used as our texture sampler name in our fragment shader. We will also add our libraries and our main javascript file just before the body closing tag.
<body>
<!-- div that will hold our WebGL canvas -->
<div id="canvas"></div>
<div id="content">
<h1 id="title">My awesome slider</h1>
<!-- drag slider -->
<div id="planes">
<div class="plane-wrapper">
<span class="plane-title">Slide 1</span>
<div class="plane">
<img src="images/slide-1.jpg" data-sampler="planeTexture" alt="First slide" />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Slide 2</span>
<div class="plane">
<img src="images/slide-2.jpg" data-sampler="planeTexture" alt="Second slide" />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Slide 3</span>
<div class="plane">
<img src="images/slide-3.jpg" data-sampler="planeTexture" alt="Third slide" />
</div>
</div>
<!-- add more slides if you want... -->
</div>
<div id="drag-tip">
Drag to explore
</div>
</div>
<script src="js/anime.min.js" type="text/javascript"></script>
<script src="js/curtains.min.js" type="text/javascript"></script>
<script src="js/slider.setup.js" type="text/javascript"></script>
</body>
CSS
Because our images will be displayed as WebGL planes in our canvas, we will have to hide the original images. Each time a plane will be created, we’ll add a “loaded” class to its parent HTML element to animate the according title opacity. Finally, we will catch errors during our WebGL initialization (or if there’s any trouble while compiling the shaders) and add a “no-curtains” class to the document body. We thus need to handle that case in the CSS to display our original images back.
@media screen {
html, body {
min-height: 100%;
}
body {
margin: 0;
font-size: 18px;
font-family: 'Arvo', Verdana, sans-serif;
background: #ece5d1;
line-height: 1.4;
overflow: hidden;
}
/*** canvas ***/
/* our canvas will have the size of our window */
#canvas {
position: fixed;
top: 0;
right: 0;
left: 0;
height: 100vh;
z-index: 1;
}
/*** content ***/
#content {
position: relative;
z-index: 2;
overflow: hidden;
}
#title {
position: fixed;
top: 20px;
right: 20px;
left: 20px;
z-index: 1;
pointer-events: none;
font-size: 1.5em;
line-height: 1;
margin: 0;
text-transform: uppercase;
color: #032f4d;
text-align: center;
}
#planes {
/* width of items * number of items */
width: calc(((100vw / 1.75) + 10vw) * 7);
padding: 0 2.5vw;
height: 100vh;
display: flex;
align-items: center;
cursor: move;
}
.plane-wrapper {
position: relative;
width: calc(100vw / 1.75);
height: 70vh;
margin: auto 5vw;
text-align: center;
}
/* disable pointer events and text selection during drag */
#planes.dragged .plane-wrapper {
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.plane-title {
position: absolute;
top: 50%;
left: 50%;
z-index: 1;
transform: translate3D(-50%, -50%, 0);
font-size: 4vw;
font-weight: 700;
line-height: 1.2;
text-transform: uppercase;
color: #032f4d;
text-stroke: 1px white;
-webkit-text-stroke: 1px white;
opacity: 0;
transition: color 0.5s, opacity 0.5s;
}
#planes.dragged .plane-title {
color: transparent;
}
.plane-wrapper.loaded .plane-title, .no-curtains .plane-title {
opacity: 1;
}
.plane {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.plane img {
/* hide original images if there's no WebGL error */
display: none;
/* prevent original image from dragging */
pointer-events: none;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
#drag-tip {
position: fixed;
right: 20px;
bottom: 20px;
left: 20px;
z-index: 1;
pointer-events: none;
font-size: 0.9em;
text-transform: uppercase;
color: #032f4d;
text-align: center;
}
/*** handling WebGL errors ***/
.no-curtains #planes {
transition: background-color 0.5s;
}
.no-curtains #planes.dragged {
background-color: #03879a;
}
.no-curtains .plane-title {
opacity: 1;
}
.no-curtains .plane {
display: flex;
overflow: hidden;
transition: filter 0.5s;
}
.no-curtains #planes.dragged .plane {
filter: grayscale(1);
}
.no-curtains .plane img {
display: block;
min-width: 100%;
min-height: 100%;
object-fit: cover;
}
}
@media screen and (orientation: portrait) {
#content {
max-height: 100vh;
}
#planes {
overflow: hidden;
width: 100vw;
padding: 2.5vh 0;
height: auto;
flex-direction: column;
}
.plane-wrapper {
width: 70vw;
height: calc(100vh / 1.75);
margin: 5vw 0;
}
.plane-title {
font-size: 10vw;
}
}
That’s it, nothing difficult here. Now let’s move on tothe WebGL!
Part 2. Shaders
Before we’ll extend the Slider class to add all the WebGL related javascript, we will write our shaders. We have 2 different elements: our planes (which will all use the same shaders) and our shader pass. We will then have to write 2 pairs of shaders.
The shaders will be put inside script tags just before our body closing tag. Pay attention to their id attributes, we’ll use them in our javascript later.
Planes
Plane vertex shader
The planes vertex shader is just positioning our planes relative to their HTML elements thanks to the projection and view matrices generated by the library. It will also pass the new texture coords to our fragment shader. By using the texture matrix uniform to calculate new texture coords we ensure that the texture will always fit the plane without breaking its natural aspect ratio.
<script id="slider-planes-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// default mandatory attributes
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
// those projection and model view matrices are generated by the library
// it will position and size our plane based on its HTML element CSS values
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
// this is generated by the library based on the sampler name we provided
// it will be used to map adjust our texture coords so the texture will fit the plane
uniform mat4 planeTextureMatrix;
// texture coord varying that will be passed to our fragment shader
varying vec2 vTextureCoord;
void main() {
// apply our vertex position based on the projection and model view matrices
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
// varying
// use texture matrix and original texture coords to generate accurate texture coords
vTextureCoord = (planeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
}
</script>
Plane fragment shader
We will map our texture to the texture coords we’ve just passed and animate its opacity based on our uOpacity uniform and the distance from each pixel to the left edge:
<script id="slider-planes-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
// our texture coords varying
varying vec2 vTextureCoord;
// our texture sampler (see how its name matches the data-sampler attribute on our image tags)
uniform sampler2D planeTexture;
// our opacity uniform that goes from 0 to 1
uniform float uOpacity;
void main( void ) {
// map our texture to the varying texture coords
vec4 finalColor = texture2D(planeTexture, vTextureCoord);
// the distance from this point to the left edge is a float from 0 to 1
float distanceToLeft = distance(vec2(0.0, vTextureCoord.y), vTextureCoord);
// calculate an effect that goes from 0 to 1 depenging on uOpacity and distanceToLeft
float spreadFromLeft = clamp((uOpacity * (1.0 - distanceToLeft) - 1.0) + uOpacity * 2.0, 0.0, 1.0);
// handle pre-multiplied alpha on rgb values and use spreadFromLeft as alpha.
finalColor = vec4(vec3(finalColor.rgb * spreadFromLeft), spreadFromLeft);
// this is it
gl_FragColor = finalColor;
}
</script>
Post processing
Shader passes use frame buffer objects to render your whole scene to a texture, apply shaders and then render this texture back to the canvas.
Most of the post processing effect will happen inside our fragment shader. We are going to use a displacement texture to spice up the overall effect. We’ll use this black and white image’s RGB values to calculate how much displacement we’ll apply to each pixel. We’re going to repeat and offset this texture so it looks like it’s following our planes. We then have to use a pattern image to obtain a seamless effect.
This is the image we’ll use:
Before we’ll have a detailed look at those shaders, let’s decompose what will occur in our shaders:
- Calculate our mouse position relative to texture coords in our vertex shader and pass it as a varying to our fragment shader.
- Calculate a spreadFromMouse float varying from 0 to 1 based on our uDragEffect uniform and the distance from the mouse to the far edges (just like we did for opacity with the planes).
- Apply a kind of fish eye effect based on spreadFromMouse (the farther from the mouse, the more distortion we’ll get). See figure 1.
- Apply a displacement based on our displacement map RGB values and on spreadFromMouse (the farther from the mouse, the more displacement). See figure 2.
- Apply a grayscale and background color effect based on spreadFromMouse. See figure 3.
Post processing vertex shader
A shader pass don’t use projection and model view matrices because the render texture (which represents what is actually drawn on the canvas) and the canvas always have the same size and position. For the same reason you won’t need to use the texture matrix on the render texture coords. We will however need to use our displacement image texture matrix to calculate its accurate texture coords.
<script id="distortion-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// default mandatory attributes
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
// our displacement texture matrix uniform
uniform mat4 displacementTextureMatrix;
// mouse position and direction uniforms
uniform vec2 uMousePos;
uniform float uDirection;
// custom varyings
varying vec2 vTextureCoord;
varying vec2 vDispTextureCoord;
varying vec2 vMouseTexCoords;
void main() {
gl_Position = vec4(aVertexPosition, 1.0);
// varyings
vTextureCoord = aTextureCoord;
vDispTextureCoord = (displacementTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
// we will handle our mouse coords here for better performance
// get our texture coords for both directions
vec2 mouseHorizontalTexCoords = (uMousePos + 1.0) / 2.0;
mouseHorizontalTexCoords.y = 0.5;
vec2 mouseVerticalTexCoords = (uMousePos + 1.0) / 2.0;
mouseVerticalTexCoords.x = 0.5;
// use the right value for the right direction
vMouseTexCoords = mix(mouseHorizontalTexCoords, mouseVerticalTexCoords, uDirection);
}
</script>
Post processing fragment shader
Here you will find all the steps we defined above. Some effects depend on the slider direction: we will calculate both effects and chose the right one based on the slider direction. We could have used if and else statements but those tend to decrease performance in GLSL and should be used with caution.
<script id="distortion-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
// varyings
varying vec2 vTextureCoord;
varying vec2 vDispTextureCoord;
varying vec2 vMouseTexCoords;
// our render texture is basically what's being drawn in our canvas
uniform sampler2D renderTexture;
// the displacement texture we've loaded into our shader pass
uniform sampler2D displacementTexture;
// all our uniforms
uniform float uDragEffect;
uniform vec2 uMousePos;
uniform vec2 uOffset;
uniform float uDirection;
uniform vec3 uBgColor;
void main( void ) {
vec2 textureCoords = vTextureCoord;
// repeat and offset our displacement map texture coords for both slider directions
vec2 horizontalPhase = fract(vec2(vDispTextureCoord.x + uOffset.x, vDispTextureCoord.y + (uOffset.y / 3000.0)) / vec2(1.0, 1.0));
vec2 verticalPhase = fract(vec2(vDispTextureCoord.x * (uOffset.x / 3000.0), vDispTextureCoord.y + uOffset.y) / vec2(1.0, 1.0));
// use the correct repeated and offseted texture coords
vec2 phase = mix(horizontalPhase, verticalPhase, uDirection);
vec4 displacement = texture2D(displacementTexture, phase);
// use our varying mouse texture coords
vec2 mouseTexCoords = vMouseTexCoords;
float distanceToMouse = distance(mouseTexCoords, textureCoords);
// calculate an effect that goes from 0 to 1 depenging on uDragEffect and distanceToMouse
float spreadFromMouse = clamp((uDragEffect * (1.0 - distanceToMouse) - 1.0) + uDragEffect * 2.0, 0.0, 1.0);
// calculate our fish eye like distortions
vec2 fishEye = (vec2(textureCoords - mouseTexCoords).xy) * pow(distanceToMouse, 2.5);
// add a displacement based on our map and our time uniform
float displacementEffect = displacement.r * 1.25;
// spread our fish eye and displacement effects from our mouse
// calculate for both directions
vec2 horizontalTexCoords = textureCoords;
horizontalTexCoords.x -= spreadFromMouse * fishEye.x * displacementEffect / 4.0;
horizontalTexCoords.y -= spreadFromMouse * fishEye.y * displacementEffect;
vec2 verticalTexCoords = textureCoords;
verticalTexCoords.x -= spreadFromMouse * fishEye.x * displacementEffect;
verticalTexCoords.y -= spreadFromMouse * fishEye.y * displacementEffect / 4.0;
// use the right value for the right direction
textureCoords = mix(horizontalTexCoords, verticalTexCoords, uDirection);
// get our final colored and BW vec4
vec4 finalColor = texture2D(renderTexture, textureCoords);
float grey = dot(finalColor.rgb, vec3(0.299, 0.587, 0.114));
vec4 finalGrey = vec4(vec3(grey), 1.0);
// mix our both vec4 based on our spread value
finalColor = mix(finalColor, finalGrey, spreadFromMouse * finalColor.a);
//finalColor.a = mix(displacementEffect * distanceToMouse, 1.0, finalColor.a);
// apply a grey background where we don't have nothing to draw
finalColor = mix(vec4(uBgColor.r * spreadFromMouse / 255.0, uBgColor.g * spreadFromMouse / 255.0, uBgColor.b * spreadFromMouse / 255.0, spreadFromMouse), finalColor, finalColor.a);
gl_FragColor = finalColor;
}
</script>
Part 3. The WebGL
Okay, now that we’ve written our shaders, let’s go back to the javascript and add the WebGL part. We will extend our Slider class (be sure to insert our Slider class code seen in the previous article before what we’ll write next) and use almost the same code structure : constructor, helpers, hooks, set up and destroy methods, except that for the sake of clarity, we’ll write all the set up functions before helper and hooks.
Initialize
Let’s start by creating a new Curtains instance. We need to pass the ID of the div that will wrap our canvas as a parameter. This will silently append a canvas, get our WebGL context, start a requestAnimationFrame loop, draw our scene, etc. It will return an object that we will use later to add our planes and shader pass.
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 = [];
this.shaderPass = null;
// set up the WebGL part
this.setupWebGL();
}
/*** WEBGL INIT ***/
// set up WebGL context and scene
setupWebGL() {
// set up our WebGL context, append the canvas to our wrapper and create a requestAnimationFrame loop
// the canvas will be our scene containing all our planes
// this is the scene we will post process
this.curtains = new Curtains("canvas");
this.curtains.onError(function() {
// onError handles all errors during WebGL context initialization or plane creation
// we will add a class to the document body to display original images (see CSS)
document.body.classList.add("no-curtains");
});
// planes and shader pass
this.setupPlanes();
this.setupShaderPass();
}
};
As you can see we are calling setupPlanes and setupShaderPass methods in our init function. We are going to code them right now.
Adding the planes
To add our planes, we will use the addPlane method of our Curtains object. This method takes 2 parameters:
- An HTML element that will be bound to the plane. The plane will copy its CSS sizes and positions. On window resize event, it will update to the new sizes and dimensions under the hood. It will also automatically create a texture for all images, canvases and videos children of that element. In our case we just have one image.
- A parameter object. This is where we’ll specify our shaders scripts IDs and our uniforms.
Once our plane has been created, we will push it into our planes array for later use. Planes have a convenient onReady event that fires once all their initial textures have been created: this is where we are going to animate their opacity and add the “loaded” class to its parent HTML element.
class WebGLSlider extends Slider {
/*** CONSTRUCTOR ***/
...
/*** WEBGL INIT ***/
...
/*** PLANES CREATION ***/
setupPlanes() {
// Planes
// each plane is bound to a HTML element to copy its size and position
// in this case this will be the slider inner items
// it will automatically create a WebGL texture for each image, canvas and video child of that element
var planeElements = document.getElementsByClassName("plane");
// our planes params
// we just pass our shaders tag ID and a uniform to animate opacity on load
var params = {
vertexShaderID: "slider-planes-vs",
fragmentShaderID: "slider-planes-fs",
uniforms: {
opacity: {
name: "uOpacity", // variable name inside our shaders
type: "1f", // this means our uniform is a float
value: 0,
},
},
};
// add all our planes and handle them
for(var i = 0; i < planeElements.length; i++) {
// addPlane method adds a plane to our WebGL scene
// takes 2 params: our HTML referent element and the params set above
// it returns a Plane class object if creation is successful, false otherwise
var plane = this.curtains.addPlane(planeElements[i], params);
// if our plane has been successfully created
if(plane) {
// push it into our planes array
this.planes.push(plane);
// onReady is called once our plane is ready and all its texture have been created
plane.onReady(function() {
// inside our onReady function scope, this represents our plane
var currentPlane = this;
// add a "loaded" class to display the title
currentPlane.htmlElement.closest(".plane-wrapper").classList.add("loaded");
// animate plane opacity once they are loaded
var opacity = {
value: 0,
};
anime({
targets: opacity,
value: 1,
easing: "linear",
duration: 750,
update: function() {
// continualy increase opacity from 0 to 1
currentPlane.uniforms.opacity.value = opacity.value;
},
});
});
}
}
}
};
Adding the shader pass
Next up is the shader pass. Adding a shader pass is even easier than adding a plane thanks to the addShaderPass method of our Curtains object. It doesn’t need to be bound to a HTML element as it will in fact be bound to our canvas instead. It will only need a parameter object with shaders scripts IDs and uniforms.
Once it has been added, we will load the displacement image into it using loadImage. This method accepts an image HTML element as parameter so we first need to create one. No need to listen to the load event of the image tho, the library will take care of that.
Finally we will use the onRender event of the shader pass to continuously offset our texture along the secondary axis.
class WebGLSlider extends Slider {
/*** CONSTRUCTOR ***/
...
/*** WEBGL INIT ***/
...
/*** PLANES CREATION ***/
...
/*** SHADER PASS CREATION ***/
setupShaderPass() {
// Shader pass
// we will post process our scene
// that means we will apply shaders to our whole scene
// like for regular planes we will need params
// they will contain vertex and fragment shaders ID and our uniforms
var shaderPassParams = {
vertexShaderID: "distortion-vs",
fragmentShaderID: "distortion-fs",
uniforms: {
// apply the whole effect
// 0: no effect
// 1: full effect
dragEffect: {
name: "uDragEffect", // variable name inside our shaders
type: "1f", // this means our uniform is a float
value: 0,
},
// our mouse position (in WebGL clip space coordinates)
mousePos: {
name: "uMousePos",
type: "2f", // this means our uniform is a length 2 array of floats
value: [0, 0],
},
// direction of our slider
// 0: horizontal drag
// 1: vertical drag
direction: {
name: "uDirection",
type: "1f",
value: this.direction,
},
// the background color when effect is applied
bgColor: {
name: "uBgColor",
type: "3f", // this means our uniform is a length 3 array of floats
value: [3, 135, 154], // rgb values
},
// our displacement texture offset
offset: {
name: "uOffset",
type: "2f",
value: [0, 0],
},
},
};
// addShaderPass adds a shader pass (Frame Buffer Object) to our WebGL scene
// returns a ShaderPass class object if successful, false otherwise
this.shaderPass = this.curtains.addShaderPass(shaderPassParams);
// if our shader pass has been successfully created
if(this.shaderPass) {
// we will add our displacement map texture
// first we load a new image
// please check the path to your displacement image!!
var image = new Image();
image.src = "images/displacement.jpg";
// then we set its data-sampler attribute to use in fragment shader
image.setAttribute("data-sampler", "displacementTexture");
// finally we load it into our shader pass via the loadImage method
this.shaderPass.loadImage(image);
var self = this;
// onRender is called at each requestAnimationFrame call
this.shaderPass.onRender(function() {
// we will continuously offset our displacement texture on secondary axis
var secondaryDirection = self.direction === 0 ? 1 : 0;
self.shaderPass.uniforms.offset.value[secondaryDirection] = self.shaderPass.uniforms.offset.value[secondaryDirection] + 1;
});
}
}
};
Using the hooks
We’ve set up all our WebGL objects, now we need to bind them to the slider thanks to the hooks we’ve declared in our Slider class. We’re going to override them with new methods.
Our planes are automatically resized when you resize your browser. That’s because curtains.js knows when a window resize event occurs and can handle the calculation of the new sizes and positions. But the library can’t know when you’re moving your planes, via CSS or javascript, and we are indeed translating their parent div. We need to tell our planes to update their positions with a simple call to the updatePosition method. We’ll put that in our onTranslation function.
We will also need to update the shader pass mouse position, drag effect and slider direction in our various helper and hook handlers.
class WebGLSlider extends Slider {
/*** CONSTRUCTOR ***/
...
/*** WEBGL INIT ***/
...
/*** PLANES CREATION ***/
...
/*** SHADER PASS CREATION ***/
...
/*** HELPER ***/
// this will update our shader pass mouse position uniform
updateMousePosUniform(mousePosition) {
// if our shader pass exists, update the mouse position uniform
if(this.shaderPass) {
// mouseToPlaneCoords converts window coordinates to WebGL clip space
var relativeMousePos = this.shaderPass.mouseToPlaneCoords(mousePosition[0], mousePosition[1]);
this.shaderPass.uniforms.mousePos.value = [relativeMousePos.x, relativeMousePos.y];
}
}
/*** HOOKS ***/
// this is called once our mousedown / touchstart event occurs and the drag started
onDragStarted(mousePosition) {
// pause and remove previous animation
if(this.animation) this.animation.pause();
anime.remove(slider);
// get a ref
var self = this;
// animate our mouse down effect
this.animation = anime({
targets: self,
effect: 1,
easing: 'easeOutCubic',
duration: self.options.duration,
update: function() {
if(self.shaderPass) {
// update our shader pass uniforms
self.shaderPass.uniforms.dragEffect.value = self.effect;
}
}
});
// enableDrawing to re-enable drawing again if we disabled it earlier
this.curtains.enableDrawing();
// update our shader pass mouse position uniform
this.updateMousePosUniform(mousePosition);
}
// this is called while we are currently dragging the slider
onDrag(mousePosition) {
// update our shader pass mouse position uniform
this.updateMousePosUniform(mousePosition);
}
// this is called once our mouseup / touchend event occurs and the drag started
onDragEnded(mousePosition) {
// calculate duration based on easing
var duration = 100 / this.options.easing;
var easing = 'linear';
// if there's no movement just tween the shader pass effect
if(Math.abs(this.translation - this.currentPosition) < 5) {
easing = 'easeOutCubic';
duration = this.options.duration;
}
// pause remove previous animation
if(this.animation) this.animation.pause();
anime.remove(slider);
// get a ref
var self = this;
this.animation = anime({
targets: self,
effect: 0,
easing: easing,
duration: duration,
update: function() {
if(self.shaderPass) {
// update drag effect
self.shaderPass.uniforms.dragEffect.value = self.effect;
}
}
});
// update our shader pass mouse position uniform
this.updateMousePosUniform(mousePosition);
}
// this is called continuously while the slider is translating
onTranslation() {
// keep our WebGL planes position in sync with their HTML elements
for(var i = 0; i < this.planes.length; i++) {
// updatePosition is a useful method that apply the HTML element position to our WebGL plane
this.planes[i].updatePosition();
}
// 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;
}
}
// this is called once the translation has ended
onTranslationEnded() {
// we will stop rendering our WebGL until next drag occurs
if(this.curtains) {
this.curtains.disableDrawing();
}
}
// this is called after our slider has been resized
onSliderResized() {
// update our direction uniform
if(this.shaderPass) {
// update direction
this.shaderPass.uniforms.direction.value = this.direction;
}
}
};
We’re almost done here. Last thing we’ll do is add a way to cleanly destroy the WebGL part of our slider and override our Slider class initial destroy method:
class WebGLSlider extends Slider {
/*** CONSTRUCTOR ***/
...
/*** WEBGL INIT ***/
...
/*** PLANES CREATION ***/
...
/*** SHADER PASS CREATION ***/
...
/*** HELPER ***/
...
/*** HOOKS ***/
...
/*** DESTROY ***/
// destroy all WebGL related things
destroyWebGL() {
// if you want to totally remove the WebGL context uncomment next line
// and remove what's after
//this.curtains.dispose();
// if you want to only remove planes and shader pass and keep the context available
// that way you could re init the WebGL later to display the slider again
if(this.shaderPass) {
this.curtains.removeShaderPass(this.shaderPass);
}
for(var i = 0; i < this.planes.length; i++) {
this.curtains.removePlane(this.planes[i]);
}
}
// call this method publicly to destroy our slider and the WebGL part
// override the destroy method of the Slider class
destroy() {
// destroy everything related to WebGL and the slider
this.destroyWebGL();
this.destroySlider();
}
};
// custom options
var options = {
easing: 0.075,
duration: 500,
dragSpeed: 1.75,
}
// let's go!
var slider = new WebGLSlider(options);
And there you have your awesome WebGL drag slider!
In the last part we’ll see how to improve the performance by removing all unnecessary layout / reflow calls.