Before / After 이미지 슬라이더
개발 •
요즘 간간이 포토샵도 만지작거리는데, 보정 열심히 끝내면 원본이랑 비교해보는 재미가 쏠쏠하더라고요.
그러다 이미지 비교엔 슬라이더만 한 게 없단 생각에, 슬라이더를 한 번 제작해봤습니다.
이래저래 찾아보니 죄다 jQuery로 만든 것뿐이더라고요. 외부 라이브러리에 의존하지 않게 제작해봤습니다.
HTML
<div class="comparison-slider">
<figure>
<img src="./images/before.jpg" alt="before" />
<figcaption>Before</figcaption>
</figure>
<figure>
<img src="./images/after.jpg" alt="after" />
<figcaption>After</figcaption>
</figure>
</div>.comparison-slider 안에 두 개의 figure를 추가하고, 그 안에 img와 figcaption을 추가했습니다.
figcaption의 추가 여부는 선택입니다.
CSS
.comparison-slider {
position: relative;
width: 100%;
margin: auto;
user-select: none;
overflow: hidden;
touch-action: pan-x;
}
.comparison-slider > figure {
margin: 0;
}
.comparison-slider > figure:last-of-type {
position: absolute;
top: 0;
left: 0;
height: 100%;
clip-path: polygon(50% 0, 100% 0, 100% 100%, 50% 100%);
}
.comparison-slider > figure > img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
pointer-events: none;
}
.comparison-slider > figure > figcaption {
position: absolute;
bottom: 0;
display: inline-block;
padding: 5px 10px;
line-height: 1.5;
background: rgba(30, 30, 30, 0.7);
max-width: 30%;
overflow: hidden;
text-overflow: ellipsis;
color: #f1f1f1;
transition:
opacity 0.35s,
transform 0.35s;
}
.comparison-slider > figure:last-of-type > figcaption {
right: 0;
}
.comparison-slider > figure > figcaption.hide {
opacity: 0;
transform: translate3d(-10px, 0, 0);
}
.comparison-slider > figure:last-of-type > figcaption.hide {
transform: translate3d(10px, 0, 0);
}
.comparison-slider > .slider {
position: absolute;
top: calc(50% - 20px);
left: 50%;
display: flex;
width: 40px;
height: 40px;
justify-content: center;
align-items: center;
border-radius: 50%;
transform: translate3d(-20px, 0, 0);
background: #f1f1f1;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.45);
text-align: center;
cursor: grab;
}
.comparison-slider.dragging,
.comparison-slider.dragging > .slider {
cursor: grabbing;
}
.comparison-slider.dragging > .slider {
background: #d2abff;
}
.comparison-slider > .slider > svg {
pointer-events: none;
}.comparison-slider에 touch-action: pan-x를 추가해 y축으론 터치가 작동하지 않게 했습니다.
반드시 화면을 꽉 채우지 않게 해야 모바일에서 스크롤이 가능합니다.
만약 모바일에서 화면을 꽉 채울 여지가 있다면 좀 못났더라도 touch-action을 제거해주셔야 합니다.
이미지의 크기 조절은
- before 이미지의 크기를 기준으로 after 이미지를 맞출 것
- before 이미지가 왼쪽, after 이미지가 오른쪽에 표시될 것
- window에 resize 이벤트를 추가할 필요가 없게 할 것
위 조건을 다 만족하려니 after 이미지를 clip-path를 이용해 자르는 게 최선이더라고요.

clip-path: polygon에서 polygon 내부의 좌표가 4개일 땐 위 그림처럼 작동합니다.
굳이 퍼센티지 계산하기 귀찮으시면 clip-path maker를 이용하시면 편하게 제작하실 수 있을 겁니다.
심지어 폴리곤은 만드시기 나름이기에 내부의 점이 꼭 4개란 법도 없고, 5개 넘어가기 시작하면 어지러워서 머리만 굴려선 만들기 힘들더라고요.
Javascript
document.querySelectorAll(".comparison-slider").forEach((element) => {
const slider = document.createElement("div");
const resizeElement = element.getElementsByTagName("figure")[1];
if (!resizeElement) return;
const figcaption = {
first: element.getElementsByTagName("figcaption")[0],
second: element.getElementsByTagName("figcaption")[1],
};
const arrow = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
let ticking = false;
const slide = (event) => {
if (!ticking) {
ticking = true;
requestAnimationFrame(() => {
ticking = false;
// sliding image
const clientX = event.clientX ?? event.touches[0].clientX;
const x = clientX - element.offsetLeft;
let percentage = ((x / element.offsetWidth) * 10000) / 100;
if (percentage >= 100) {
percentage = 100;
}
if (percentage <= 0) {
percentage = 0;
}
slider.style.left = `${percentage}%`;
resizeElement.style.clipPath = `polygon(${percentage}% 0, 100% 0, 100% 100%, ${percentage}% 100%)`;
// hiding figcaption
if (figcaption.first) {
if (x <= figcaption.first.offsetWidth) {
figcaption.first.classList.add("hide");
} else {
figcaption.first.classList.remove("hide");
}
}
if (figcaption.second) {
if (element.offsetWidth - x <= figcaption.second.offsetWidth) {
figcaption.second.classList.add("hide");
} else {
figcaption.second.classList.remove("hide");
}
}
});
}
};
const dragStart = () => {
element.addEventListener("mousemove", slide, { passive: true });
element.addEventListener("touchmove", slide, { passive: true });
element.classList.add("dragging");
};
const dragDone = () => {
element.removeEventListener("mousemove", slide);
element.removeEventListener("touchmove", slide);
element.classList.remove("dragging");
};
slider.addEventListener("mousedown", dragStart, { passive: true });
slider.addEventListener("touchstart", dragStart, { passive: true });
document.addEventListener("mouseup", dragDone, { passive: true });
document.addEventListener("touchend", dragDone, { passive: true });
document.addEventListener("touchcancel", dragDone, { passive: true });
slider.classList.add("slider");
arrow.setAttribute("width", "20");
arrow.setAttribute("height", "20");
arrow.setAttribute("viewBox", "0 0 30 30");
path.setAttribute("d", "M1,14.9l7.8-7.6v4.2h12.3V7.3l7.9,7.6l-7.9,7.7v-4.2H8.8v4.2L1,14.9z");
arrow.append(path);
slider.append(arrow);
element.append(slider);
});slide를 그냥 호출하면 초당 450번까지도 레이아웃이 일어나길래 requestAnimationFrame을 이용해 디스플레이의 주사율에 맞게 레이아웃이 업데이트되게 최적화를 진행했습니다.
마우스를 클릭하거나 터치를 시작한 상태에선 좌우로 아무리 크게 움직여도 슬라이더를 움직일 수 있게 mousemove과 touchmove는 element에 이벤트를 추가했지만 mouseup, touchend는 document에 이벤트를 추가했습니다.
화살표 이모지 하나 추가하는데 굳이 svg를 썼습니다. 폰트마다 화살표 높이가 이상하게 달라 화살표가 정중앙에 오지 않는 폰트가 많더라고요.
여담으로, createElement로는 svg와 path를 만들 수 없더라고요. createElementNS를 처음 써봤습니다.
사족
슬라이더에 넣은 이미지가 태어나서 처음 해본 인물 보정인데, 개인적으로 꽤 괜찮게 결과가 나온 것 같네요.
4x 업스케일, 색감 보정, 입술 덧칠, 치마 색 수정 등을 진행했습니다.
