https://spicyyoghurt.com/tools/easing-functions
easing function 은 게임에서 캐릭터를 부드럽게 이동시킬때 자주 사용되는 개념이다. 물체를 화면에서 부드럽게 이동시켜야 하는 경우 적용할 수 있다. 여기서는 웹 화면에서 자주 사용되는 커로셀이나 슬라이드 화면을 구현하기 위하여 다양한 easing function 을 실험해볼수 있다.
알고리즘의 핵심은 현재 화면에서 걸쳐있는 슬라이드가 다음 버튼을 클릭하면 맨 앞으로 이동한다. 이전 버튼을 클릭하면 화면에 걸쳐있던 슬라이드가 다시 화면에 걸치게 된다. 현재는 5번 슬라이드가 맨 앞이나 뒤로 간다.
디바이스 크기가 줄어들면 현재 화면에 걸쳐있는 슬라이드도 변경된다. 현재는 4번 슬라이드가 화면에 걸쳐있으므로 이전/다음 버튼을 클릭하면 4번 슬라이드가 맨 앞이나 맨 뒤로 이동한다.
알고리즘을 구현하기 위하여 슬라이드를 감싸고 있는 전체 컨테이너 너비를 카드 너비로 나눈다. 그럼 화면에 걸쳐있는 슬라이드를 제외하고 컨테이너에서 완전히 보이는 슬라이드의 갯수를 계산할 수 있다. 이때 카드 사이에 마진이 존재하므로 카드 너비로 나눌때 마진도 포함해서 계산한다. 그런 다음 카드 너비만큼 다시 곱해주면 이전/다음 버튼 클릭시 얼마만큼 이동해야 하는지 거리를 구할수 있다.
예를 들어 현재 컨테이너 너비가 500px 이고 카드너비가 100px 이고 마진이 10px 이라고 가정하자! 500 / (100 + 10) = 4.5xxx 가 계산된다. 여기서 정수인 부분은 4이므로 4개의 슬라이드가 컨테이너 내부에서 가리지 않고 완전히 보인다. 그럼 4 * (100 + 10) = 440px 이 계산된다. 즉, 이전/다음 버튼 클릭시 해당 거리만큼 이동하게 된다. 디바이스 크기가 변하면 컨테이너 너비와 카드 너비를 동적으로 가져오므로 이전/다음 버튼 클릭시 이동할 거리는 새로 계산된다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="actions">
<span class="prev"><</span>
<span class="next">></span>
</div>
<div class="carousel">
<div class="card">1</div>
<div class="card">2</div>
<div class="card">3</div>
<div class="card">4</div>
<div class="card">5</div>
<div class="card">6</div>
<div class="card">7</div>
<div class="card">8</div>
<div class="card">9</div>
<div class="card">10</div>
<div class="card">11</div>
<div class="card">12</div>
<div class="card">13</div>
<div class="card">14</div>
<div class="card">15</div>
<div class="card">16</div>
<div class="card">17</div>
<div class="card">18</div>
<div class="card">19</div>
<div class="card">20</div>
</div>
</div>
<script src='app.js'></script>
</body>
</html>
.container{
width: 50%; height: 300px;
position: relative;
}
span{
position: absolute;
top: 50%; transform: translateY(-50%);
color: white;
font-size: 2rem;
background-color: rgba(0, 0, 0, .4);
width: 30px; height: 30px;
line-height: 30px;
border-radius: 50%;
text-align: center;
cursor: pointer;
z-index: 1;
user-select: none;
font-size: 1rem;
font-weight: bold;
}
span.prev{
left: 1rem;
}
span.next{
right: 1rem;
}
.carousel{
width: 100%; height: 100%;
display: flex;
grid-gap: 10px;
background-color: #fff;
overflow-x: scroll;
}
.card{
min-width: 200px; height: 100%;
background-color: aquamarine;
text-align: center;
line-height: 300px;
font-weight: bold;
font-size: 2rem;
color: white;
/* border: 1px solid red; */
}
const prevBtn = document.querySelector('.prev')
const nextBtn = document.querySelector('.next')
const carousel = document.querySelector('.carousel')
const container = document.querySelector('.container')
const card = carousel.querySelector('.card')
const cards = carousel.querySelectorAll('.card')
const margin = 10
let timerId = null
let currentTime = 0
let startPoint = 0
function changePosition(unitTime, start, destination){
currentTime += unitTime
if(currentTime < 1){
console.log('ongoing')
// console.log(easeOutExpo (currentTime, start, destination, 1))
carousel.scrollLeft = easeOutQuint(currentTime, start, destination-start, 1)
// carousel.scrollLeft = destination
prevBtn.removeEventListener('click', toPrevSlides)
nextBtn.removeEventListener('click', toNextSlides)
console.log(start, destination)
}else{
console.log('end')
currentTime = 0
clearInterval(timerId)
prevBtn.addEventListener('click', toPrevSlides)
nextBtn.addEventListener('click', toNextSlides)
startPoint = destination
console.log(startPoint)
}
}
function toPrevSlides(){
// console.log('prev')
const distance = Math.floor(container.offsetWidth / (card.offsetWidth + margin)) * (card.offsetWidth + margin)
let destination = startPoint-distance
// console.log(distance, destination, startPoint)
if(destination >= 0)
timerId = setInterval(() => changePosition(0.01, startPoint, destination), 20)
else
destination = 1
}
function toNextSlides(){
const distance = Math.floor(container.offsetWidth / (card.offsetWidth + margin)) * (card.offsetWidth + margin)
let destination = startPoint+distance
// console.log('distance: ', distance)
// console.log(cards.length * (card.offsetWidth+margin))
if(destination < cards.length * (card.offsetWidth+margin))
timerId = setInterval(() => changePosition(0.01, startPoint, destination), 20)
else
destination = cards.length * (card.offsetWidth+margin)
}
prevBtn.addEventListener('click', toPrevSlides)
nextBtn.addEventListener('click', toNextSlides)
function easeOutExpo (t, b, c, d) {
return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
}
function easeLinear (t, b, c, d) {
return c * t / d + b;
}
// function easeInOutQuad (t, b, c, d) {
// if ((t /= d / 2) < 1) return c / 2 * t * t + b;
// return -c / 2 * ((--t) * (t - 2) - 1) + b;
// }
function easeInOutQuad(t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t + b;
t--;
return -c/2 * (t*(t-2) - 1) + b;
}
function easeInOutCubic (t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
return c / 2 * ((t -= 2) * t * t + 2) + b;
}
function easeInOutSine (t, b, c, d) {
return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
}
function easeOutQuad (t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
}
function easeOutCirc (t, b, c, d) {
return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
}
function easeOutCubic (t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
}
function easeOutQuart (t, b, c, d) {
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
}
function easeOutQuint (t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
}
'프론트엔드 > 컴포넌트' 카테고리의 다른 글
입력창에서 숫자만 입력 가능하게 하기 (36) | 2023.08.24 |
---|---|
말풍선 툴팁 & 드롭다운 메뉴 만들기 (0) | 2023.07.29 |
로딩 애니메이션 (0) | 2023.07.05 |
부드러운 가로 스크롤링 (2) (0) | 2023.07.05 |
부드러운 가로 스크롤링 (0) | 2023.06.27 |