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
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>
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;
}
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',
});
Cool! Now, if you save it, you should see this in the browser.
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