Project Moon - GSAP 커서 애니메이션 + 탐색 메뉴 + WebGl 슬라이더
39934 단어 webdevjavascripttutorialbeginners
프로젝트 목적
가까운 미래를 위한 나의 JavaScript 테스팅 그라운드.
시작하기
계속해서 CodePen playground을 사용하여 새 프로젝트를 초기화하거나 src 폴더 아래에 다음 파일 구조를 사용하여 Visual Studio Code에서 자신의 프로젝트를 설정하십시오.
Project Moon Starter Files
|- Assets
|- CSS
|- style.css
|- JS
|- main.js
|- /src
|- index.html
파트 1: HTML
index.html을 편집하여 시작하고 다음 코드로 바꿉니다.
<!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>
파트 2: CSS
다음 단계는 다음 스타일을 추가하고 style.css 파일을 완성하는 것입니다.
@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;
}
파트 3: 자바스크립트
이제 WebGL 설정에 자바스크립트 로직을 구현할 수 있습니다.
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);
요약
따라했다면 프로젝트를 완료하고 WebGL 프로젝트를 완료했을 것입니다.
이제 여기까지 했다면 코드를 내GitHub에 연결하여 포크하거나 복제하면 작업이 완료됩니다.
라이선스: 📝
이 프로젝트는 MIT 라이선스(MIT)에 따릅니다. 자세한 내용은 라이센스를 참조하십시오.
기여
기여는 언제나 환영합니다...
🔹 저장소 포크
🔹 현재 프로그램 개선
🔹 기능 개선
🔹 새로운 기능 추가
🔹 버그 수정
🔹 작업 푸시 및 풀 리퀘스트 생성
유용한 리소스
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
Reference
이 문제에 관하여(Project Moon - GSAP 커서 애니메이션 + 탐색 메뉴 + WebGl 슬라이더), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/hr21don/project-moon-gsap-cursor-animation-navigation-menu-webgl-slider-57dh텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)