How to create a wave displacement WebGL effect
How to create a wave displacement WebGL effect
Jan 1, 2021 | Read time 6 minutes🔔 Table of contents
See the demo here
In this article, I will be doing a portfolio concept design with a displacement filter effect on the main hero image. Displacement filters are not particularly well known, I just know it from my time using PhotoShop. I will be using the PixiJS library to handle this since they have a default “DisplacementFilter” function that we can play around with.
(Note: I havent had the time to make this fully responsive - so please excuse me :))
What the heck is a displacement filter?
A displacement filter can be used to achieve some crazy warping effects. It distorts pixels from one image using another image called the displacement map. Displacement maps are usually images that are grayscale. I will be using the ‘ripple.png’ as my displacement map.
So in our case with PixiJs, when we call the displacement filter function, it takes the values from our displacement map to look up the correct values to output. As an example:
HTML
The HTML for this is not too complicated - the main action is in the Javascript and PixiJS
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Create a wave displacement WebGL effect</title>
<link rel="stylesheet" href="style.css">
<script src="https://unpkg.com/ionicons@4.5.10-0/dist/ionicons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300&display=swap" rel="stylesheet">
</head>
<script src="pixi.min.js"></script>
<body>
<div class="container">
<div class="nav">
<div class="search">
<ion-icon name="search"></ion-icon>
</div>
<div class="menu">
<ion-icon name="more"></ion-icon>
</div>
</div>
<div class="header">
<h1>Portfolio</h1>
</div>
<div class="media">
<ul>
<li>
<ion-icon name="logo-facebook"></ion-icon>
</li>
<li>
<ion-icon name="logo-instagram"></ion-icon>
</li>
<li>
<ion-icon name="logo-twitter"></ion-icon>
</li>
</ul>
</div>
<div class="share">
<ion-icon name="share"></ion-icon>
</div>
<div class="bottomnav">
<ul>
<li>
<ion-icon id="arrow-down" name="arrow-down"></ion-icon>
</li>
<li>
<ion-icon id="arrow-up" name="arrow-up"></ion-icon>
</li>
</ul>
</div>
</div>
<script type="text/javascript" src="main.js">
</script>
</body>
</html>
CSS
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
}
body {
margin: 0;
padding: 0;
}
.nav {
position: fixed;
width: 97%;
height: 80px;
}
.brand, .search, .menu {
position: absolute;
line-height: 80px;
}
.brand {
margin-left: 40px;
font-family: 'Montserrat', sans-serif;
color: #fff;
font-size: 16px;
}
.search {
right: 140px;
color: #fff;
}
.menu{
right: 40px;
font-family: 'Montserrat', sans-serif;
color: #fff;
font-size: 20px;
}
.media {
position: absolute;
right: 0;
top: 50%;
transform: rotate(90deg);
}
.media ul {
list-style: none;
}
.media ul li {
display: inline-block;
text-transform: uppercase;
color: rgba(255, 255, 255, .7);
font-family: 'Montserrat', sans-serif;
font-size: 1.5rem;
font-weight: 300;
letter-spacing: 2px;
margin-right: 40px;
}
.share {
position: absolute;
bottom: 30px;
left: 40px;
color: #fff;
font-size: 20px;
}
.header {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-family: 'Montserrat', sans-serif;
font-size: 40px;
letter-spacing: 3px;
}
h1 span {
-webkit-text-stroke: 1px #fff;
color: rgba(0,0,0,0);
}
.bottomnav {
position: fixed;
width: 15%;
height: 80px;
bottom: -40px;
background: #fff;
left: 70%;
transform: translate(-50%, -50%);
}
.bottomnav ul {
margin: 0;
padding: 0;
list-style: none;
line-height: 80px;
float: right;
}
.bottomnav ul li {
padding-right: 50px;
display: inline-block;
}
Javascript with PixiJs
In the JavaScript I am doing the following things:
-
Initializing PixiJs and add the app to the document body
-
Create a bunch of textures that we want to use. In this case, the textures are my Evangelion background images :)
-
Load the displacement map (ripple.png) to PixiJs.
-
Continually loop and update the filter and texture. Everytime we loop, we also calculate the mouse movement velocity so that we can add a scalling effect to the filter.
var app = new PIXI.Application(window.innerWidth, window.innerHeight);
document.body.appendChild(app.view);
// create a texture from an image path
var textureA = PIXI.Texture.fromImage('a.jpg');
// create a second texture
var textureB = PIXI.Texture.fromImage('b.jpg');
var textureC = PIXI.Texture.fromImage('c.jpg');
var textureD = PIXI.Texture.fromImage('d.jpg');
app.stage.interactive = true;
var posX, displacementSprite, displacementFilter, bg, vx;
var container = new PIXI.Container();
app.stage.addChild(container);
PIXI.loader.add("ripple.png").load(setup);
function setup() {
posX = app.renderer.width / 2;
displacementSprite = new PIXI.Sprite(PIXI.loader.resources["ripple.png"].texture);
displacementFilter = new PIXI.filters.DisplacementFilter(displacementSprite);
displacementSprite.anchor.set(0.5);
displacementSprite.x = app.renderer.width / 2;
displacementSprite.y = app.renderer.height / 2;
vx = displacementSprite.x;
app.stage.addChild(displacementSprite);
container.filters = [displacementFilter];
displacementFilter.scale.x = 0;
displacementFilter.scale.y = 0;
bg = new PIXI.Sprite(textureA);
bg.height = app.renderer.height;
bg.width = 700
container.addChild(bg);
app.stage.on('mousemove', onPointerMove).on('touchmove', onPointerMove);
loop();
}
var arrowDown = document.getElementById('arrow-down');
var clickCount = 0;
arrowDown.addEventListener('click', function () {
if (clickCount == 0) {
bg.texture = textureB
} else if (clickCount == 1) {
bg.texture = textureC
} else if (clickCount == 2) {
bg.texture = textureD
} else if (clickCount == 3) {
bg.texture = textureA;
clickCount = 0;
return;
}
clickCount++;
})
function onPointerMove(eventData) {
posX = eventData.data.global.x;
}
function loop() {
requestAnimationFrame(loop);
vx += (posX - displacementSprite.x) * 0.045;
displacementSprite.x = vx;
var disp = Math.floor(posX - displacementSprite.x);
if (disp < 0) disp = -disp;
var fs = map(disp, 0, 500, 0, 120);
disp = map(disp, 0, 500, 0.1, 0.6);
displacementSprite.scale.x = disp;
displacementFilter.scale.x = fs;
}
map = function (n, start1, stop1, start2, stop2) {
var newval = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
return newval;
};
// GSAP animate the header
TweenMax.from(".header h1", 2, {
delay: 1,
y: 20,
opacity: 0,
ease: Expo.easeInOut
});