Before / After 이미지 슬라이더
개발 •
![Before / After 이미지 슬라이더](https://cdn-t.marshallku.com/images/images/2020/09/image-comparison-slider.png)
요즘 간간이 포토샵도 만지작거리는데, 보정 열심히 끝내면 원본이랑 비교해보는 재미가 쏠쏠하더라고요.
그러다 이미지 비교엔 슬라이더만 한 게 없단 생각에, 슬라이더를 한 번 제작해봤습니다.
이래저래 찾아보니 죄다 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](https://cdn-t.marshallku.com/images/images/2020/09/polygon.png)
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 업스케일, 색감 보정, 입술 덧칠, 치마 색 수정 등을 진행했습니다.