배너 이미지

Before / After 이미지 슬라이더

최종 수정일 : (11개월 전)

Github

요즘 간간이 포토샵도 만지작거리는데, 보정 열심히 끝내면 원본이랑 비교해보는 재미가 쏠쏠하더라고요.

그러다 이미지 비교엔 슬라이더만 한 게 없단 생각에, 슬라이더를 한 번 제작해봤습니다.
이래저래 찾아보니 죄다 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를 추가하고, 그 안에 imgfigcaption을 추가했습니다.
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-slidertouch-action: pan-x를 추가해 y축으론 터치가 작동하지 않게 했습니다.
반드시 화면을 꽉 채우지 않게 해야 모바일에서 스크롤이 가능합니다.
만약 모바일에서 화면을 꽉 채울 여지가 있다면 좀 못났더라도 touch-action을 제거해주셔야 합니다.


이미지의 크기 조절은

  1. before 이미지의 크기를 기준으로 after 이미지를 맞출 것
  2. before 이미지가 왼쪽, after 이미지가 오른쪽에 표시될 것
  3. window에 resize 이벤트를 추가할 필요가 없게 할 것

위 조건을 다 만족하려니 after 이미지를 clip-path를 이용해 자르는 게 최선이더라고요.

clip-path polygon

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 (!resizeElementreturn;
    const figcaption = {
        firstelement.getElementsByTagName("figcaption")[0],
        secondelement.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, { passivetrue });
        element.addEventListener("touchmove"slide, { passivetrue });
        element.classList.add("dragging");
    };
    const dragDone = () => {
        element.removeEventListener("mousemove"slide);
        element.removeEventListener("touchmove"slide);
        element.classList.remove("dragging");
    };

    slider.addEventListener("mousedown"dragStart, { passivetrue });
    slider.addEventListener("touchstart"dragStart, { passivetrue });

    document.addEventListener("mouseup"dragDone, { passivetrue });
    document.addEventListener("touchend"dragDone, { passivetrue });
    document.addEventListener("touchcancel"dragDone, { passivetrue });

    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을 이용해 디스플레이의 주사율에 맞게 레이아웃이 업데이트되게 최적화를 진행했습니다.

마우스를 클릭하거나 터치를 시작한 상태에선 좌우로 아무리 크게 움직여도 슬라이더를 움직일 수 있게 mousemovetouchmoveelement에 이벤트를 추가했지만 mouseup, touchenddocument에 이벤트를 추가했습니다.

화살표 이모지 하나 추가하는데 굳이 svg를 썼습니다. 폰트마다 화살표 높이가 이상하게 달라 화살표가 정중앙에 오지 않는 폰트가 많더라고요.


여담으로, createElement로는 svg와 path를 만들 수 없더라고요. createElementNS를 처음 써봤습니다.


사족

슬라이더에 넣은 이미지가 태어나서 처음 해본 인물 보정인데, 개인적으로 꽤 괜찮게 결과가 나온 것 같네요.
4x 업스케일, 색감 보정, 입술 덧칠, 치마 색 수정 등을 진행했습니다.


profile

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

주의 : 비밀 댓글 사용 시 수정 기능을 이용할 수 있는 시간이 지나면 작성자도 내용 확인이 불가능합니다.