Draggable carousel with WebGL effects

What will you learn

Magda Odrowąż-Żelezik
Fink IT
10 min readMay 18, 2020

--

How to make draggable slider/carousel with WebGL distortion effect like on example below

Technologies we will use:

  • JavaScript (duh) ES6 (here uses in React component)
  • PIXI.js library for WebGL
  • GSAP library for animations
  • some CSS with SCSS preprocessor

Overview

We will setup the carousel with JSX (with tips how to do it in pure HTML/JS workflow). Then we will style it. In the next step, we will apply drag to the carousel. Next, we will import PIXI library and add a canvas to every item in the slider with some distortions prepared. Lastly, using GSAP, we will animate the distortions while dragging action is performed.

The setup

Basic setup is a div with a bunch of children with content. Div may have display:flex so that the children are forced to go next to each other or some other CSS way to achieve it can be used.

I strongly encourage to have your data separated from your view. Your code is way more flexible this way and you are able to modify your data easily without navigating through more or less complicated layout structure. I find it a good practice.

Therefore, we will start setting up the project with JSON file with our data:

There you have an array of object with the same properties like type, url, title etc. Thumb property is a path to image that will be an element of the carousel. Url will enable redirection on click. The rest of attributes is just an example how you can extend your json to store more than you actually need for view (but you may actually need somewhere else).

All you need to do now in your code it to import the json list:

and iterate through it in your jsx structure:

Setting up carousel in jsx

If you are reproducing the slider without React or any library/framework, go ahead and place your slides directly to HTML or insert them with appendChild through JS.

Note that in the example above the thumbnail picture is not inserted yet into the DOM. We will use WebGL for that.

Let’s style this up a bit:

First, we secure that the outermost div will be as wide as it can fit the viewport.

Inside it we place actual drag-able area. I gave it display flex, but it could also be done in many other ways. The idea is to have the children next to each other.

Direct children of dragg-area are actually anchors. I gave them some additional margin to be able to drag easily.

Now, note that inside .thumb element I style canvas element. There is no canvas element yet in the DOM, but it will appear in the next step.

The dragging

I implemented simple dragging function based on this issue on stackoverflow.

This solution is desktop-only, which is not ideal, but can be easily adapted to touch devices with some extra event listeners.

First, set up few variables:

Then, set three functions: startDrag(), dragDiv() and endDrag();

Let’s start with start :)

Disclaimer: you could do this differently. Like, attach listener directly to the dragged object. I am going to have multiple sliders on one page, so this one seems valid to me.

The code is already explained in the comments, but let me clarify the function flow once again: first, you target the object you are performing and event on (it will be onmousedown). Then you check if this target belongs to the actual group you wish to have the dragging enabled. Then you store the current position of the cursor (offsetX) and current left corner position of the target (coordX). The you set drag to true, since we are starting the dragging now. Lastly, you attach onmousemove the next function, which will actually handle the recalculations of cursor and targets coordinates.

The function performed onmousemove is short:

In first line, we ensure it doesn’t perform just onmousemove, without holding the left key. That is why we needed drag flag (swag hehe). Since we don’t pass argument when calling this function in startDrag, we can ensure it’s value here, we want to relate to window.event. Then actually calculation happens — we change the target position to new one, based on it’s previous position plus cursor’s current position minus cursor’s previous position.

We are almost done now. In the next step we need to switch the flag to false onmouseup (after you release the left mouse key).

Let’s wrap it this up now and sew everything together:

And this is it! You just did a dragging function.

Image displacement using PIXI.js

PIXI comes with almost ready solution how to do an image displacement on their webpage. What we will do is to take this solution and apply it to all of images in the slider at once.

How the solution works

  1. You set up PIXI app and container (you attach new canvas to the DOM)
  2. To the canvas you apply your image as Sprite, you position it etc.
  3. To the canvas you add displacement sprite, which is a texture that will influence your original image
  4. You create a displacement filter based on that texture, which (filter) you apply to the original image
  5. You animate the displacement sprite scale or rotation to create a movement of the original image
Example code from PIXI.js

What you need to do

You need to ensure the solution is applied to all of your thumbnail pictures. Therefore, you have to create new canvas for each of them, add image to each of them, add displacement sprite to each of them, etc.

First, let’s access the slides

I have a React app, so I am going to reference to DOM objects a little bit differently. I you’re working sauté, you just go for classic getElementById.

My whole thing is wrapped in one div:

In React, I cannot access the DOM element with .get…() function, since the DOM will not be rendered before the function is performed and this action will result in an error. What I can do, is to refer to the element via reference.

I am here creating a variable, which will store my reference to DOM object. I just have to link them together:

Now I can access it easily in my displacement creating function and THEN target my thumbs with getElementsByClassName function:

Now, the thumbs array stores all the elements where we would like to place our canvases in.

Do all setups at once

Thumbs stores you DOM elements, but you need a storage for PIXI objects also. This way it would be easy to create all of them at once and also, later, to animate all of them at once. Let’s use a simple array for that:

I saw this variable new in a CodePen fiddle somewhere and I loved it.

Let’s now create a wrap function for the setup of the playgrounds:

Inside, you will iterate over all of your thumbnails and for each slide create a PIXI renderer with a canvas. Then you will setup all the properties necessary like in example above.

Inside the displacements function let’s create a loop over thumbs collection. Note, that thumbs is a HTMLCollection, so it could be iterated over with regular loops, but I would like to use on it more effective array method — map. To do that, you need to convert the HTMLCollection to Array:

First step is to set up a renderer and append it to each thumbnail. Last line might be less intuitive, but let me explain — since we are taking all the thumbnails from the list array to the DOM, we can reference their paths also by iterating over the same list. Once we run over thumbnails, the index of current iteration can be used to extract the thumbnails path from the JSON list array.

Next step is to create main sprite and center in in the canvas. Lastly, always remember to add it to playground object.

Next, you need to set up the displacement sprite. In this particular example I use texture I made from free image from textures.com:

Texture I use

This texture is not seamless and too “realistic” to normally serve the purpose of displacement filter. However, this suits my needs well since I use it enlarged on much smaller images, so it does not need to be seamless or abstract. Usually you would rather go for textures like so:

Textures from textures.com

You are almost done. Be sure to add the playground object to the playgrounds array:

and actually launch the function. You should first ensure that you DOM object exists. Let’s do this in window.onload:

Animating the texture

Nothing is going to happen just yet — first you need to animate the thing.

Let’s create animate() function, which we will loop though requestAnimationFrame:

On each iteration we increase the displacementSprite.scale a little bit. This should create some movement for starters. Let’s run this function for the first time just after we finish setting up playground object:

Cute. We would like the animations to run on drag though. Let’s remove that two lines which animate the texture now:

and switch to GSAP. The animate function will serve the purpose of re-rendering the whole thing every single frame.

Animating on demand

If you are not familiar with GSAP, get familiar with it like: right now. It’s amazing, efficient library and you will be sorry you did animations without it ever.

https://gph.is/g/Zr9X8da

Let’s start with start drag. Let’s say we want the image to reach on touch, staring spreading the waves — the value must be increasing then. Let me remind you, the staring value of texture scale is 0.

We will keep the duration 1 second, because we would like to keep the animation going during the drag also — and it will be best to control that in the dragging function itself. Another thing requires explanation here: ease function. It was created by GSAP easing generator and looks like this:

Let’s switch now to dragDiv function and do some tweening there. It is going to be a little more tricky however. We should make the animation dependent on the two factors — direction (whether we are going right or left) and distance (to make the distortions a little bit more responsive).

Let’s set up two variables then:

And tweak the code to animate the sprites accordingly:

Lastly, we need to reset everything in the stopDrag function:

Note, that I do not reset scale to 0 — I would like to keep some displacement after the movement to mess things up a bit. If you would like to return to default, set it to 0 of course.

And that’s it!

Wrapping it up

The values for set up and animation can be tweaked to suit your personal needs. I myself got there just trying and adjusting. Feel free to play with it, that’s why it is called playground :)

I don’t really like the idea that I need to iterate over an array each time to perform a frame of animation. This appears very inefficient to me, especially with larger collections in mind. I might sit down to it again and figure out a more elegant solution, but for starters I believe this solution can be reproduced even if you’re complete beginner.

Best of luck!

This article is based on a part of my engineer thesis. All the screenshots (except stated as not) belong to me and depict my original work.

--

--

Magda Odrowąż-Żelezik
Fink IT

Creative front end developer, currently excited about learning 3D graphics. Visit magdazelezik.com