An Interest In:
Web News this Week
- April 2, 2024
- April 1, 2024
- March 31, 2024
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
Project Moon - GSAP Cursor Animation Navigation Menu WebGl Slider
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 optionsvar 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
Original Link: https://dev.to/hr21don/project-moon-gsap-cursor-animation-navigation-menu-webgl-slider-57dh
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To