Purpose Of Project
My JavaScript Testing ground for the foreseeable future.
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.
Project Moon Starter Files
|- Assets
|- CSS
|- style.css
|- JS
|- main.js
|- /src
|- index.html
Part 1: HTML
Start by editing your index.html and replace it with the following code.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Project Moon | Navigation + Slider</title>
<link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAAwAEAAOADAADn8wAA8+cAAPHHAAD5zwAA+I8AAPyfAAD8HwAA/j8AAP4/AAD/fwAA//8AAP//AAD//wAA" rel="icon" type="image/x-icon" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<!--Cursor-->
<div>
<div class="cursor"></div>
<div class="cursorDot"></div>
</div>
<main>
<!-- Start Navigation -->
<header id="header">
<div class="header-row">
<div class="brand-logo">
<a class="brand-text cursor-scale small" href="#">Project Moon</a>
</div>
<div class="main cursor-scale small">
<div class="bars"></div>
</div>
<div class="menu">
<div class="navBefore"></div>
<div class="nav">
<ul class="navigation">
<li><a href="#" class="cursor-scale">Home</a></li>
<li><a href="#" class="cursor-scale">About</a></li>
<li><a href="#" class="cursor-scale">Work</a></li>
<li><a href="#" class="cursor-scale">Contact</a></li>
<li><a target="_blank" href="#">EN</a></li>
</ul>
</div>
</div>
</div>
</header>
<section id="content">
<div id="planes">
<div class="plane-wrapper">
<span class="plane-title">JAPAN</span>
<div class="plane">
<img src="https://images.unsplash.com/photo-1545569341-9eb8b30979d9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NHx8amFwYW58ZW58MHx8MHx8&auto=format&fit=crop&w=600&q=60" alt="Photo by Su San Lee on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">AUSTRALIA</span>
<div class="plane">
<img src="https://images.unsplash.com/photo-1506973035872-a4ec16b8e8d9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8YXVzdHJhbGlhfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=600&q=60" alt="Photo by Dan Freeman on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">USA</span>
<div class="plane">
<img src="https://images.unsplash.com/photo-1591437009328-f4499ddd7eb0?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTV8fHRleGFzJTIwZmxhZ3xlbnwwfHwwfHw%3D&auto=format&fit=crop&w=600&q=60" alt="Photo by Aaron Burden on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">UK</span>
<div class="plane">
<img src="https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8ZW5nbGFuZHxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=600&q=60" alt="Photo by Ben Davies on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
</div>
</section>
</main>
<!-- GSAP CDN -->
<script src="https://unpkg.co/gsap@3/dist/gsap.min.js"></script>
<!-- CurtainJS CDN -->
<script src="https://www.curtainsjs.com/build/curtains.min.js"></script>
<!-- AnimeJS CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/2.2.0/anime.min.js"></script>
<!-- Core theme JS-->
<script src="assets/js/main.js"></script>
</body>
</html>
Part 2: CSS
Next step is to add the following styles and complete our style.css file.
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Orbitron:wght@400;500;600;700;800;900&display=swap');
/* Base reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
}
/*Body styling*/
body {
font-family: "Orbitron", sans-serif;
letter-spacing: 2px;
line-height: 2;
background-color: black;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/*Nav Styles*/
#header{
position: fixed;
z-index: 10;
left: 0;
top: 0;
width: 100%;
height: 100vh;
}
.header-row{
padding: 0px 15px;
display: flex;
justify-content: space-between;
}
/*Brand Logo + text*/
.brand-logo{
line-height: 100px;
float: left;
text-transform: uppercase;
}
.brand-text {
font-size: 2em;
line-height: 80px;
font-family: "Montserrat", cursive;
font-weight: 500;
text-decoration-line: none;
color: #fff;
}
/*Hamburger Styles*/
.main .bars {
position: fixed;
height: 30px;
width: 50px;
top: 5%;
right: 5%;
display: flex;
flex-direction: column;
align-items: center;
z-index: 9999999999;
cursor: pointer;
}
.main .bars::before {
position: absolute;
content: "";
height: 2px;
width: 90%;
background: #fff;
transition: 0.3s linear;
}
.main .bars.active::before {
transform: rotate(45deg);
width: 50%;
top: 5%;
background: #000;
}
.main .bars::after {
position: absolute;
content: "";
height: 2px;
width: 90%;
background: #fff;
top: 35%;
transition: 0.3s linear;
}
.main .bars.active::after {
transform: rotate(-45deg);
width: 50%;
top: 5%;
background: #000;
}
/*Nav Menu*/
.menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999999999;
overflow: hidden;
display: none;
}
.menu .navBefore {
position: absolute;
margin-left: 100%;
width: 100%;
height: 100%;
background: #017bf5;
}
.menu .nav {
position: relative;
margin-left: 100%;
width: 100%;
height: 100%;
background: #fff;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.menu .nav ul {
opacity: 0;
}
.menu .nav ul li {
list-style: none;
}
.menu .nav ul li a {
position: relative;
font-size: 4.5rem;
text-decoration: none;
text-align: center;
color: #666;
}
.menu .nav ul li a:hover,
.menu .nav ul li.active a {
color: #000;
text-decoration-line: line-through;
}
/* Cursor Styles*/
.cursor{
position: absolute;
width: 40px;
height: 40px;
margin-left: -20px;
margin-top: -20px;
border-radius: 50%;
border: 3px solid whitesmoke;
transform: translate(-50%, -50%);
transition: transform .2s ease;
pointer-events: none;
backdrop-filter: grayscale(1);
z-index: 1000;
}
.cursorDot{
position: absolute;
width: 4px;
height: 4px;
margin-left: -20px;
margin-top: -20px;
border-radius: 50%;
background-color: whitesmoke;
transform: translate(-50%, -50%);
transition: 0.1s;
pointer-events: none;
z-index: 1000;
}
.grow, .grow-small{
transform: scale(4);
background: white;
mix-blend-mode: difference;
border: none;
}
.grow-small{
transform: scale(2);
}
/*Drag Slider*/
#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: #fff;
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;
}
Part 3: JavaScript
Now we can implement our JavaScript logic to our WebGL setup like so.
console.clear();
let cursor = document.querySelector('.cursor');
let cursorDot = document.querySelector(".cursorDot");
let cursorScale = document.querySelectorAll('.cursor-scale');
let mouseX = 0;
let mouseY = 0;
gsap.to({}, 0.016, {
repeat: -1,
onRepeat: function(){
gsap.set(cursor, {
css: {
left: mouseX,
top: mouseY,
}
});
gsap.set(cursorDot, {
css: {
left: mouseX,
top: mouseY
}
});
}
});
window.addEventListener("mousemove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
cursorScale.forEach((link) => {
link.addEventListener("mousemove", () => {
cursor.classList.add("grow");
if (link.classList.contains("small")) {
cursor.classList.remove("grow");
cursor.classList.add("grow-small");
}
});
link.addEventListener("mouseleave", () => {
cursor.classList.remove("grow");
cursor.classList.remove("grow-small");
});
});
window.onload = function () {
const bars = document.querySelector(".bars");
const menu = document.querySelector(".menu");
bars.addEventListener("click", function (e) {
this.classList.toggle("active");
if (this.classList.contains("active")) {
gsap.to(".menu", {
duration: 0.1,
display: "flex",
ease: "expo.in"
});
gsap.to(".navBefore", {
duration: 0.5,
marginLeft: "0",
ease: "expo.in"
});
gsap.to(".nav", {
duration: 0.8,
marginLeft: "0",
delay: 0.3,
ease: "expo.in"
});
gsap.to(".navigation", {
duration: 1,
opacity: "1",
delay: 0.8,
ease: "expo.in"
});
} else {
gsap.to(".navigation", {
duration: 0.2,
opacity: "0",
ease: "expo.in"
});
gsap.to(".nav", {
duration: 1,
marginLeft: "100%",
delay: 0.3,
ease: "expo.in"
});
gsap.to(".navBefore", {
duration: 1,
marginLeft: "100%",
delay: 0.5,
ease: "expo.in"
});
gsap.to(".menu", {
duration: 1,
display: "none",
delay: 1,
ease: "expo.in"
});
}
});
};
class Slider {
/*** CONSTRUCTOR ***/
constructor(options = {}) {
// our options
this.options = {
// slider state and values
// the div we are going to translate
element: options.element || document.getElementById("planes"),
// easing value, the lower the smoother
easing: options.easing || 0.1,
// translation speed
// 1: will follow the mouse
// 2: will go twice as fast as the mouse, etc
dragSpeed: options.dragSpeed || 1,
// duration of the in animation
duration: options.duration || 750,
};
// if we are currently dragging
this.isMouseDown = false;
// if the slider is currently translating
this.isTranslating = false;
// current position
this.currentPosition = 0;
// drag start position
this.startPosition = 0;
// drag end position
this.endPosition = 0;
// slider translation
this.translation = 0;
this.animationFrame = null;
// set up the slider
this.setupSlider();
}
/*** HELPERS ***/
// lerp function used for easing
lerp(value1, value2, amount) {
amount = amount < 0 ? 0 : amount;
amount = amount > 1 ? 1 : amount;
return (1 - amount) * value1 + amount * value2;
}
// return our mouse or touch position
getMousePosition(e) {
var mousePosition;
if(e.targetTouches) {
if(e.targetTouches[0]) {
mousePosition = [e.targetTouches[0].clientX, e.targetTouches[0].clientY];
}
else if(e.changedTouches[0]) {
// handling touch end event
mousePosition = [e.changedTouches[0].clientX, e.changedTouches[0].clientY];
}
else {
// fallback
mousePosition = [e.clientX, e.clientY];
}
}
else {
mousePosition = [e.clientX, e.clientY];
}
return mousePosition;
}
// set the slider boundaries
// we will translate it horizontally in landscape mode
// vertically in portrait mode
setBoundaries() {
if(window.innerWidth >= window.innerHeight) {
// landscape
this.boundaries = {
max: -1 * this.options.element.clientWidth + window.innerWidth,
min: 0,
sliderSize: this.options.element.clientWidth,
referentSize: window.innerWidth,
};
// set our slider direction
this.direction = 0;
}
else {
// portrait
this.boundaries = {
max: -1 * this.options.element.clientHeight + window.innerHeight,
min: 0,
sliderSize: this.options.element.clientHeight,
referentSize: window.innerHeight,
};
// set our slider direction
this.direction = 1;
}
}
/*** HOOKS ***/
// this is called once our mousedown / touchstart event occurs and the drag started
onDragStarted(mousePosition) {
}
// this is called while we are currently dragging the slider
onDrag(mousePosition) {
}
// this is called once our mouseup / touchend event occurs and the drag started
onDragEnded(mousePosition) {
}
// this is called continuously while the slider is translating
onTranslation() {
}
// this is called once the translation has ended
onTranslationEnded() {
}
// this is called before our slider has been resized
onBeforeResize() {
}
// this is called after our slider has been resized
onSliderResized() {
}
/*** ANIMATIONS ***/
// this will translate our slider HTML element and set up our hooks
translateSlider(translation) {
translation = Math.floor(translation * 100) / 100;
// should we translate it horizontally or vertically?
var direction = this.direction === 0 ? "translateX" : "translateY";
// apply translation
this.options.element.style.transform = direction + "(" + translation + "px)";
// if the slider translation is different than the translation to apply
// that means the slider is still translating
if(this.translation !== translation) {
// hook function to execute while we are translating
this.onTranslation();
}
else if(this.isTranslating && !this.isMouseDown) {
// if those conditions are met, that means the slider is no longer translating
this.isTranslating = false;
// hook function to execute after translation has ended
this.onTranslationEnded();
}
// finally set our translation
this.translation = translation;
}
// this is our request animation frame loop where we will translate our slider
animate() {
// interpolate values
var translation = this.lerp(this.translation, this.currentPosition, this.options.easing);
// apply our translation
this.translateSlider(translation);
this.animationFrame = requestAnimationFrame(this.animate.bind(this));
}
/*** EVENTS ***/
// on mouse down or touch start
onMouseDown(e) {
// start dragging
this.isMouseDown = true;
// apply specific styles
this.options.element.classList.add("dragged");
// get our touch/mouse start position
var mousePosition = this.getMousePosition(e);
// use our slider direction to determine if we need X or Y value
this.startPosition = mousePosition[this.direction];
// drag start hook
this.onDragStarted(mousePosition);
}
// on mouse or touch move
onMouseMove(e) {
// if we are not dragging, we don't do nothing
if(!this.isMouseDown) return;
// get our touch/mouse position
var mousePosition = this.getMousePosition(e);
// get our current position
this.currentPosition = this.endPosition + ((mousePosition[this.direction] - this.startPosition) * this.options.dragSpeed);
// if we're not hitting the boundaries
if(this.currentPosition > this.boundaries.min && this.currentPosition < this.boundaries.max) {
// if we moved that means we have started translating the slider
this.isTranslating = true;
}
else {
// clamp our current position with boundaries
this.currentPosition = Math.min(this.currentPosition, this.boundaries.min);
this.currentPosition = Math.max(this.currentPosition, this.boundaries.max);
}
// drag hook
this.onDrag(mousePosition);
}
// on mouse up or touchend
onMouseUp(e) {
// we have finished dragging
this.isMouseDown = false;
// remove specific styles
this.options.element.classList.remove("dragged");
// update our end position
this.endPosition = this.currentPosition;
// send our mouse/touch position to our hook
var mousePosition = this.getMousePosition(e);
// drag ended hook
this.onDragEnded(mousePosition);
}
// on resize we will need to apply old translation value to new sizes
onResize(e) {
this.onBeforeResize();
// get our old translation ratio
var ratio = this.translation / this.boundaries.sliderSize;
// reset boundaries and properties bound to window size
this.setBoundaries();
// reset all translations
this.options.element.style.transform = "tanslate3d(0, 0, 0)";
// calculate our new translation based on the old translation ratio
var newTranslation = ratio * this.boundaries.sliderSize;
// clamp translation to the new boundaries
newTranslation = Math.min(newTranslation, this.boundaries.min);
newTranslation = Math.max(newTranslation, this.boundaries.max);
// apply our new translation
this.translateSlider(newTranslation);
// reset current and end positions
this.currentPosition = newTranslation;
this.endPosition = newTranslation;
// call our resize hook
this.onSliderResized();
}
/*** SET UP AND DESTROY ***/
// set up our slider
// init its boundaries, add event listeners and start raf loop
setupSlider() {
this.setBoundaries();
// event listeners
// mouse events
window.addEventListener("mousemove", this.onMouseMove.bind(this), {
passive: true,
});
window.addEventListener("mousedown", this.onMouseDown.bind(this));
window.addEventListener("mouseup", this.onMouseUp.bind(this));
// touch events
window.addEventListener("touchmove", this.onMouseMove.bind(this), {
passive: true,
});
window.addEventListener("touchstart", this.onMouseDown.bind(this), {
passive: true,
});
window.addEventListener("touchend", this.onMouseUp.bind(this));
// resize event
window.addEventListener("resize", this.onResize.bind(this));
// launch our request animation frame loop
this.animate();
}
// will be called silently to cleanly remove the slider
destroySlider() {
// remove event listeners
// mouse events
window.removeEventListener("mousemove", this.onMouseMove, {
passive: true,
});
window.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mouseup", this.onMouseUp);
// touch events
window.removeEventListener("touchmove", this.onMouseMove, {
passive: true,
});
window.removeEventListener("touchstart", this.onMouseDown, {
passive: true,
});
window.removeEventListener("touchend", this.onMouseUp);
// resize event
window.removeEventListener("resize", this.onResize);
// cancel request animation frame
cancelAnimationFrame(this.animationFrame);
}
// call this method publicly to destroy our slider
destroy() {
// destroy everything related to the slider
this.destroySlider();
}
};
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 = [];
// we will keep track of the previous translation values on resize
this.previousTranslation = {
x: 0,
y: 0,
};
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({
container: "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();
}
/*** 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;
},
});
});
}
}
}
/*** 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
var image = new Image();
image.src = "https://images.unsplash.com/photo-1545569341-9eb8b30979d9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NHx8amFwYW58ZW58MHx8MHx8&auto=format&fit=crop&w=600&q=60";
// 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;
});
}
}
/*** 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() {
// get our slider translation and take our previous translation into account
var planeTranslation = {
x: this.direction === 0 ? this.translation - this.previousTranslation.x : 0,
y: this.direction === 1 ? this.translation - this.previousTranslation.y : 0,
};
// keep our WebGL planes position in sync with their HTML elements
for(var i = 0; i < this.planes.length; i++) {
// in the previous CodePen we were using updatePosition the method which handles positioning automatically
// however this method internally calls getBoundingClientRect() which causes a reflow and therefore impacts performance
// so we will position our planes manually with setRelativePosition instead, which does not trigger a layout repaint call
this.planes[i].setRelativePosition(planeTranslation.x, planeTranslation.y);
}
// 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() {
// we need to update our previous translation value
this.previousTranslation = {
x: this.direction === 0 ? this.translation : 0,
y: this.direction === 1 ? this.translation : 0,
};
// reset our slides relative positions
// because during the resize their positions has already been updated internally
for(var i = 0; i < this.planes.length; i++) {
this.planes[i].setRelativePosition(0, 0);
}
// update our direction uniform
if(this.shaderPass) {
// update direction
this.shaderPass.uniforms.direction.value = this.direction;
}
}
/*** 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.1,
duration: 500,
dragSpeed: 1.75,
}
// let's go!
var slider = new WebGLSlider(options);
Recap
If you followed along then you should have completed the project and finished off your WebGL project.
Now if you made it this far, then I am linking the code to my GitHub 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/
https://www.curtainsjs.com/build/curtains.min.js
https://cdnjs.com/libraries/gsap
https://cdnjs.cloudflare.com/ajax/libs/animejs/2.2.0/anime.min.js