DEV Community

Cover image for WebGL Liquid Distortion Image Hover Effect - Three Part Setup
Helitha Rupasinghe
Helitha Rupasinghe

Posted on

WebGL Liquid Distortion Image Hover Effect - Three Part Setup

Today, I'll teach you how to create a liquid distortion image effect using ThreeJS and TweenMaxJS with two main images and one displacement image to create the effect.

Getting started

Go ahead and initialise our new project using the CodePen playground or setup your own project on Visual Studio Code with the following file structure under your src folder.

WebGL Image Hover Master
  |- Images
    |- Image1.png
    |- Image2.png
    |- dist.png
  |- Js
    |- hover.js
    |- script.js
  |- /src
    |- index.html
    |- style.css
Enter fullscreen mode Exit fullscreen mode

Part 1: HTML

Start by editing your index.html and replace it with the following code.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-with, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="./style.css" />
    <title>WebGl| Liquid Distortion Effect</title>
</head>
<body>
    <div class="landing">
        <div class="distortion"></div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js" integrity="sha256-lPE3wjN2a7ABWHbGz7+MKBJaykyzqCbU96BJWjio86U=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/106/three.min.js" integrity="sha256-tAVw6WRAXc3td2Esrjd28l54s3P2y7CDFu1271mu5LE=" crossorigin="anonymous"></script>
    <script>!(function (f, l) {
  "object" == typeof exports && "undefined" != typeof module
    ? (module.exports = l(require("three"), require("gsap/TweenMax")))
    : "function" == typeof define && define.amd
    ? define(["three", "gsap/TweenMax"], l)
    : (f.hoverEffect = l(f.THREE, f.TweenMax));
})(this, function (f, l) {
  return (
    (l = l && l.hasOwnProperty("default") ? l.default : l),
    function (h) {
      function F() {
        for (var f = arguments, l = 0; l < arguments.length; l++)
          if (void 0 !== f[l]) return f[l];
      }
      console.log(
        "%c Hover effect by Robin Delaporte: https://github.com/robin-dela/hover-effect ",
        "color: #bada55; font-size: 0.8rem"
      );
      var w = h.parent,
        L = h.displacementImage,
        M = h.image1,
        P = h.image2,
        U = F(h.intensity1, h.intensity, 1),
        V = F(h.intensity2, h.intensity, 1),
        C = F(h.angle, Math.PI / 4),
        D = F(h.angle1, C),
        S = F(h.angle2, 3 * -C),
        W = F(h.speedIn, h.speed, 1.6),
        _ = F(h.speedOut, h.speed, 1.2),
        z = F(h.hover, !0),
        q = F(h.easing, Expo.easeOut),
        G = F(h.video, !1);
      if (w)
        if (M && P && L) {
          var A = new f.Scene(),
            B = new f.OrthographicCamera(
              w.offsetWidth / -2,
              w.offsetWidth / 2,
              w.offsetHeight / 2,
              w.offsetHeight / -2,
              1,
              1e3
            );
          B.position.z = 1;
          var k = new f.WebGLRenderer({ antialias: !1, alpha: !0 });
          k.setPixelRatio(window.devicePixelRatio),
            k.setClearColor(16777215, 0),
            k.setSize(w.offsetWidth, w.offsetHeight),
            w.appendChild(k.domElement);
          var J = function () {
              k.render(A, B);
            },
            K = new f.TextureLoader();
          K.crossOrigin = "";
          var N = K.load(L, J);
          if (((N.wrapS = N.wrapT = f.RepeatWrapping), G)) {
            var Q = function () {
              requestAnimationFrame(Q), k.render(A, B);
            };
            Q(),
              ((G = document.createElement("video")).autoplay = !0),
              (G.loop = !0),
              (G.src = M),
              G.load();
            var X = document.createElement("video");
            (X.autoplay = !0), (X.loop = !0), (X.src = P), X.load();
            var Y = new f.VideoTexture(G),
              Z = new f.VideoTexture(X);
            (Y.magFilter = Z.magFilter = f.LinearFilter),
              (Y.minFilter = Z.minFilter = f.LinearFilter),
              X.addEventListener(
                "loadeddata",
                function () {
                  X.play(),
                    ((Z = new f.VideoTexture(X)).magFilter = f.LinearFilter),
                    (Z.minFilter = f.LinearFilter),
                    ($.uniforms.texture2.value = Z);
                },
                !1
              ),
              G.addEventListener(
                "loadeddata",
                function () {
                  G.play(),
                    ((Y = new f.VideoTexture(G)).magFilter = f.LinearFilter),
                    (Y.minFilter = f.LinearFilter),
                    ($.uniforms.texture1.value = Y);
                },
                !1
              );
          } else
            (Y = K.load(M, J)),
              (Z = K.load(P, J)),
              (Y.magFilter = Z.magFilter = f.LinearFilter),
              (Y.minFilter = Z.minFilter = f.LinearFilter);
          var $ = new f.ShaderMaterial({
              uniforms: {
                intensity1: { type: "f", value: U },
                intensity2: { type: "f", value: V },
                dispFactor: { type: "f", value: 0 },
                angle1: { type: "f", value: D },
                angle2: { type: "f", value: S },
                texture1: { type: "t", value: Y },
                texture2: { type: "t", value: Z },
                disp: { type: "t", value: N },
              },
              vertexShader:
                "\nvarying vec2 vUv;\nvoid main() {\n  vUv = uv;\n  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}\n",
              fragmentShader:
                "\nvarying vec2 vUv;\n\nuniform float dispFactor;\nuniform sampler2D disp;\n\nuniform sampler2D texture1;\nuniform sampler2D texture2;\nuniform float angle1;\nuniform float angle2;\nuniform float intensity1;\nuniform float intensity2;\n\nmat2 getRotM(float angle) {\n  float s = sin(angle);\n  float c = cos(angle);\n  return mat2(c, -s, s, c);\n}\n\nvoid main() {\n  vec4 disp = texture2D(disp, vUv);\n  vec2 dispVec = vec2(disp.r, disp.g);\n  vec2 distortedPosition1 = vUv + getRotM(angle1) * dispVec * intensity1 * dispFactor;\n  vec2 distortedPosition2 = vUv + getRotM(angle2) * dispVec * intensity2 * (1.0 - dispFactor);\n  vec4 _texture1 = texture2D(texture1, distortedPosition1);\n  vec4 _texture2 = texture2D(texture2, distortedPosition2);\n  gl_FragColor = mix(_texture1, _texture2, dispFactor);\n}\n",
              transparent: !0,
              opacity: 1,
            }),
            y = new f.PlaneBufferGeometry(w.offsetWidth, w.offsetHeight, 1),
            b = new f.Mesh(y, $);
          A.add(b),
            z &&
              (w.addEventListener("mouseenter", j),
              w.addEventListener("touchstart", j),
              w.addEventListener("mouseleave", O),
              w.addEventListener("touchend", O)),
            window.addEventListener("resize", function (f) {
              k.setSize(w.offsetWidth, w.offsetHeight);
            }),
            (this.next = j),
            (this.previous = O);
        } else console.warn("One or more images are missing");
      else console.warn("Parent missing");
      function j() {
        l.to($.uniforms.dispFactor, W, {
          value: 1,
          ease: q,
          onUpdate: J,
          onComplete: J,
        });
      }
      function O() {
        l.to($.uniforms.dispFactor, _, {
          value: 0,
          ease: q,
          onUpdate: J,
          onComplete: J,
        });
      }
    }
  );
});
//# sourceMappingURL=hover-effect.umd.js.map
</script>
    <script src="./script.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Part 2: CSS

Next step is to add the following styles and complete our style.css file. Just make sure the div that will wrap the canvas fits the document, and apply any size you want to your plane div element.

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body{
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background: white;
}
.landing {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 50%;
  height: 50vh;
  box-shadow: 0 32px 32px -8px rgba(0, 0, 0, 0.5);
}
.distortion {
  position: fixed;
    width: 50vw;
    height: 50vh;
}
Enter fullscreen mode Exit fullscreen mode

Part 3: JavaScript

Now we can implement our JavaScript logic to our ThreeJS setup like so.

new hoverEffect({
  parent: document.querySelector('.distortion'),
  intensity: 0.2,
  image1: 'https://images.unsplash.com/photo-1608501078713-8e445a709b39?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8d2FsbHBhcGVyJTIwNGt8ZW58MHx8MHx8&auto=format&fit=crop&w=600&q=60', 
  image2: 'https://images.unsplash.com/photo-1580617971627-cffa74e39d1d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwcm9maWxlLXBhZ2V8OTR8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=600&q=60',
  speedIn: 2,
  speedOut: 5,
  angle1 : Math.PI / 6,
  angle2 : -Math.PI / 6 * 3,
  displacementImage: 'https://i.postimg.cc/QNTRDRks/4.png',
  //displacementImage: 'https://i.ibb.co/tbSbc6k/clouds.jpg',
});
Enter fullscreen mode Exit fullscreen mode

Cool! Now, if you save it, you should see this in the browser.

WebGL.png

Recap

If you followed along then you should have completed the project and finished off your liquid distortion effect.

Now if you made it this far, then I am linking the code to my Sandbox for you to fork or clone and then the job's done.

License: 📝

This project is under the MIT License (MIT). See the LICENSE for more information.

Contributions

Contributions are always welcome...

🔹 Fork the repository
🔹 Improve current program by
🔹 improving functionality
🔹 adding a new feature
🔹 bug fixes
🔹 Push your work and Create a Pull Request

Useful Resources

https://cdnjs.com/libraries/three.js/
https://cdnjs.com/libraries/gsap

Discussion (0)