Experience/[Javascript] JS 30

[Javascript] 27. Click and Drag

winCow 2021. 5. 11. 15:39

1. 개요

클릭과 드래그를 이용해 HTML 요소를 넘기는 듯한 효과를 준다.

 

 

2. HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Click and Drag</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="items">
    <div class="item item1">01</div>
    <div class="item item2">02</div>
    <div class="item item3">03</div>
    <div class="item item4">04</div>
    <div class="item item5">05</div>
    <div class="item item6">06</div>
    <div class="item item7">07</div>
    <div class="item item8">08</div>
    <div class="item item9">09</div>
    <div class="item item10">10</div>
    <div class="item item11">11</div>
    <div class="item item12">12</div>
    <div class="item item13">13</div>
    <div class="item item14">14</div>
    <div class="item item15">15</div>
    <div class="item item16">16</div>
    <div class="item item17">17</div>
    <div class="item item18">18</div>
    <div class="item item19">19</div>
    <div class="item item20">20</div>
    <div class="item item21">21</div>
    <div class="item item22">22</div>
    <div class="item item23">23</div>
    <div class="item item24">24</div>
    <div class="item item25">25</div>
  </div>
  </body>
</html>

 

 

3. CSS

html {
  box-sizing: border-box;
  background: url('https://source.unsplash.com/NFs6dRTBgaM/2000x2000') fixed;
  background-size: cover;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-family: sans-serif;
  font-size: 20px;
  margin: 0;
}

.items {
  height: 800px;
  padding: 100px;
  width: 100%;
  border: 1px solid white;
  overflow-x: scroll;
  overflow-y: hidden;
  white-space: nowrap;
  user-select: none;
  cursor: pointer;
  transition: all 0.2s;
  transform: scale(0.98);
  will-change: transform;
  position: relative;
  background: rgba(255,255,255,0.1);
  font-size: 0;
  perspective: 500px;
}

.items.active {
  background: rgba(255,255,255,0.3);
  cursor: grabbing;
  cursor: -webkit-grabbing;
  transform: scale(1);
}

.item {
  width: 200px;
  height: calc(100% - 40px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 80px;
  font-weight: 100;
  color: rgba(0,0,0,0.15);
  box-shadow: inset 0 0 0 10px rgba(0,0,0,0.15);
}

.item:nth-child(9n+1) { background: dodgerblue;}
.item:nth-child(9n+2) { background: goldenrod;}
.item:nth-child(9n+3) { background: paleturquoise;}
.item:nth-child(9n+4) { background: gold;}
.item:nth-child(9n+5) { background: cadetblue;}
.item:nth-child(9n+6) { background: tomato;}
.item:nth-child(9n+7) { background: lightcoral;}
.item:nth-child(9n+8) { background: darkslateblue;}
.item:nth-child(9n+9) { background: rebeccapurple;}

.item:nth-child(even) { transform: scaleX(1.31) rotateY(40deg); }
.item:nth-child(odd) { transform: scaleX(1.31) rotateY(-40deg); }

 

 

4. Javascript

  const slider = document.querySelector('.items');
  let isDown = false;
  let startX;
  let scrollLeft;

  slider.addEventListener('mousedown', (e) => {
    isDown = true;
    slider.classList.add('active');
    startX = e.pageX - slider.offsetLeft;
    scrollLeft = slider.scrollLeft;
  });
  slider.addEventListener('mouseleave', () => {
    isDown = false;
    slider.classList.remove('active');
  });
  slider.addEventListener('mouseup', () => {
    isDown = false;
    slider.classList.remove('active');
  });
  slider.addEventListener('mousemove', (e) => {
    if (!isDown) return;
    e.preventDefault();
    const x = e.pageX - slider.offsetLeft;
    const walk = (x - startX) * 3;
    slider.scrollLeft = scrollLeft - walk;
  });

먼저 효과를 줄 아이템들을 slider에 할당한다. isDown 변수로 플래그를 세우고, startX, scrollLeft 변수를 할당한다.

mousedown, mouseleave, mouseup, mousemove에 모두 이벤트 리스너를 걸고, 각각 콜백함수를 설정한다. mouseleave와 mouseup은 플래그를 false로 만들고 active 클래스를 제거하는 역할을 한다.

 

  slider.addEventListener('mousedown', (e) => {
    isDown = true;
    slider.classList.add('active');
    startX = e.pageX - slider.offsetLeft;
    scrollLeft = slider.scrollLeft;
  });
  slider.addEventListener('mousemove', (e) => {
    if (!isDown) return;
    e.preventDefault();
    const x = e.pageX - slider.offsetLeft;
    const walk = (x - startX) * 3;
    slider.scrollLeft = scrollLeft - walk;
  });

이 중, slider가 클릭, 드래그에 반응하도록 하려면 mousedown과 mousemove를 연결해야 한다.

mousedown에 대한 콜백함수는 플래그를 true로 변경하고, slider에 active 클래스 속성을 추가한다. 앞에서 만들어 둔 startX에 e.pageX - slider.offsetLeft를 할당하는데, MouseEvent.pageX는 전체 문서의 왼쪽 끝을 기준으로 마우스를 클릭한 지점의 x좌표를 반환한다. 여기에는 현재 보이지 않는 문서의 부분도 포함된다. 즉, startX는 클릭한 지점의 전체 문서에서의 x좌표에서, 전체 문서의 왼쪽 끝으로부터 아이템까지의 거리를 뺀 값이 되므로, 아이템의 왼쪽 끝을 기준(0)으로 한 x좌표라고 볼 수 있다. scrollLeft는 slider의 scrollLeft, 즉 스크롤이 오른쪽으로 이동한 총 거리를 의미한다.

mousemove에 대한 콜백함수 역시 x를 e.pageX - slider.offsetLeft로 할당하는데, 마우스가 드래그 중인 상태임을 생각하면 이는 startX와는 다르게 점점 줄어들게 될 것이다. startX는 처음 클릭된 위치의 x좌표이고 x는 왼쪽으로 이동 중인 x의 현재 시점에서의 좌표이기 때문이다. walk에 이 두 x 사이의 차이를 할당하면, walk값은 시작점으로부터 이동한 거리가 될 것이고, 처음에 클릭한 지점을 기준으로, 마우스가 왼쪽으로 이동할수록(오른쪽으로 드래그 될수록) x값은 줄어들고 walk가 커진다. x축에서 시작점으로부터 이동한 거리인 x값을 scrollLeft에 할당하게 되면, 그만큼 아이템이 드래그되게 되지만, 마우스 방향과 반대로 움직이게 설정해야 하므로 walk를 음수로 할당한다.