<div class="main">
    <div class="main__inner">
        <h1 class="main__ttl">
            <a href="https://nocebo.jp/post-1680/" target="_blank">モーダル機能を作成する</a>
        </h1>
        <div class="slider">
            <nav class="slider__nav">
                <a href="javascript:;" class="slider__prev"><i class="fas fa-angle-left"></i></a>
                <a href="javascript:;" class="slider__zoom"><i class="fas fa-search-plus"></i></a>
                <a href="javascript:;" class="slider__next"><i class="fas fa-angle-right"></i></a>
            </nav>
            <div class="slider__inner">
                <div class="slider__main">
                    <ul class="slider__list">
                        <li class="slider__elm slider__elm--01">
                            <a href="javascript:;">
                                <img src="https://nocebo.jp/data/zoomslider/img/01.jpg" alt="images">
                            </a>
                        </li>
                        <li class="slider__elm slider__elm--02">
                            <a href="javascript:;">
                                <img src="https://nocebo.jp/data/zoomslider/img/02.jpg" alt="images">
                            </a>
                        </li>
                        <li class="slider__elm slider__elm--03">
                            <a href="javascript:;">
                                <img src="https://nocebo.jp/data/zoomslider/img/03.jpg" alt="images">
                            </a>
                        </li>
                    </ul>
                </div>
            </div>
        </div>
        <!-- ====================================__  .slider  ============================= -->
        <div class="modal">
            <nav class="modal__nav">
                <a href="javascript:;" class="modal__close"><i class="fas fa-times"></i></a>
            </nav>
            <div class="modal__elm">
                <div class="zoom">
                    <div class="zoom__info">
                        <img src="https://nocebo.jp/data/zoomslider/img/01.jpg" alt="images">
                        <div class="zoom__origin zoom__origin--02"></div>
                        <div class="zoom__area"></div>
                        <div class="zoom__data">
                            <p class="zoom__op">origin<span></span></p>
                            <p class="zoom__cp">move<span></span></p>
                            <p class="zoom__scale">scale<span></span></p>
                        </div>
                    </div>
                    <div class="zoom__elm">
                        <div class="zoom__origin zoom__origin--01"></div>
                        <img src="https://nocebo.jp/data/zoomslider/img/01.jpg" alt="images">
                    </div>
                </div>
                <!-- ====================================__  .zoom  ============================= -->
            </div>
        </div>
        <!-- ====================================__  .modal  ============================= -->
    </div>
</div>
<!-- ====================================__  .main  ============================= -->
*
{
    box-sizing: border-box;

    text-decoration: none;

    touch-action: none;
}

img
{
    user-select: none;
    pointer-events: none;
}


$baseWidth: 768px;

.contents
{
    height: 100%;
}

.main
{
    display: flex;

    width: $baseWidth;
    min-height: 100%;
    margin: 0 auto;

    background-color: #eee;

    align-items: center;

    .main__inner
    {
        padding: 50px 0;
    }

    .main__ttl
    {
        font-size: 30px;
        font-weight: bold;

        margin-bottom: 20px;
        margin-left: 10px;

        color: #000;
    }
}


.slider
{
    position: relative;

    width: $baseWidth;

    .slider__inner
    {
        overflow: hidden;

        width: 100%;
    }

    .slider__elm
    {
        img
        {
            vertical-align: bottom;
        }
    }

    .slider__nav
    {
        position: absolute;
        z-index: 10;
        bottom: 0;

        display: flex;

        width: 100%;

        justify-content: space-between;

        a
        {
            font-size: 30px;
            font-weight: bold;

            display: block;

            width: 33.2%;
            padding: 25px;
            padding-bottom: 20px;

            text-align: center;
            text-decoration: none;

            color: #000;
            background-color: rgba(#fff, .7);

            &:hover
            {
                background-color: rgba(#fff, 1);
            }
        }
    }

    .slider__list
    {
        display: flex;

        width: max-content;

        li
        {
            width: $baseWidth;

            img
            {
                width: 100%;

                pointer-events: none;
            }
        }
    }
}


.modal
{
    position: fixed;
    z-index: -1;
    top: 0;
    left: 0;

    visibility: hidden;

    width: 100%;
    height: 100%;

    transition: all .2s linear;

    opacity: 0;
    background-color: rgba(0, 0, 0, .9);

    &.active
    {
        z-index: 1000;

        visibility: visible;

        opacity: 1;
    }

    .modal__nav
    {
        position: absolute;
        top: 30px;
        right: 20px;
    }

    .modal__close
    {
        font-size: 50px;

        position: fixed;
        z-index: 10000;
        top: 10px;
        right: 10px;

        width: 80px;
        height: 80px;
        padding: 23px;
        padding-top: 15px;

        color: #000;
        border-radius: 3px;
        border-radius: 100%;
        background-color: #fff;
    }

    .modal__elm
    {
        display: flex;

        height: 100%;

        align-items: center;
    }
}

.zoom
{
    position: relative;

    overflow: hidden;

    width: $baseWidth;
    height: $baseWidth;
    margin: 0 auto;

    border: 1px solid #ccc;

    .zoom__elm
    {
        width: 100%;

        &:hover
        {
            cursor: grab;
        }

        img
        {
            width: 100%;

            vertical-align: bottom;
        }
    }

    .zoom__info
    {
        position: absolute;
        z-index: 10;
        top: 1%;
        left: 1%;

        width: $baseWidth / 5;
        height: $baseWidth / 5;

        border: 1px solid #ccc;
        background-color: rgba(255, 255, 255, .4);

        img
        {
            width: 100%;

            opacity: .5;

            filter: grayscale(50%);
        }
    }

    .zoom__origin
    {
        position: absolute;
        z-index: 100;
        top: 0;
        left: 0;

        width: 20px;
        height: 20px;
        margin-top: -10px;
        margin-left: -10px;

        border-radius: 100%;
        background-color: rgba(255, 0, 0, .5);

        &--02
        {
            width: 10px;
            height: 10px;
            margin-top: -5px;
            margin-left: -5px;
        }
    }

    .zoom__area
    {
        position: absolute;
        top: 0;
        left: 0;

        width: 100%;
        height: 100%;

        transform-origin: 100% 100%;

        border: 1px solid #ccc;
        background-color: rgba(0, 0, 0, .3);
    }

    .zoom__data
    {
        font-size: 15px;

        position: absolute;
        top: -1px;
        left: $baseWidth / 5 + 10;

        width: 200px;

        text-align: left;

        color: #fff;

        p
        {
            line-height: 1.3;

            margin-bottom: 5px;
        }
        span
        {
            display: inline-block;

            margin-left: 5px;
        }
    }
}
View Compiled
// /* ===========================================================
// # Point
// =========================================================== */
class Point {
    constructor(x = 0, y = 0) {
        this.x = x || 0;
        this.y = y || 0;
    }
    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
    }
    static interpolate(p1, p2, t) {
        const x = p1.x * (1 - t) + p2.x * t;
        const y = p1.y * (1 - t) + p2.y * t;
        return new Point(x, y);
    }
    add(p) {
        this.x += p.x;
        this.y += p.y;
        return this;
    }
    subtract(p) {
        this.x -= p.x;
        this.y -= p.y;
        return this;
    }
    clone() {
        return new Point(this.x, this.y);
    }
}
// /* ===========================================================
// # EventTouch
// =========================================================== */
class EventTouch {
    constructor() {
        if (EventTouch.instance) return;
        EventTouch.instance = this;
        // -----------------------------
        this.isDown = false; //PC用

        this.currentPoint = new Point();
        this.startPoint = new Point();
        this.diffPoint = new Point();

        this.touchesdiff = [];
        this.touchesStartPoints = [new Point(), new Point()];
        this.touchesCurrentPoints = [new Point(), new Point()];

        this.scale = 1;

        // PC
        document.addEventListener("mousedown", this.onStart.bind(this), false);
        document.addEventListener("mousemove", this.onMove.bind(this), false);
        document.addEventListener("mouseup", this.onUp.bind(this), false);
        // SP
        document.addEventListener("touchstart", this.onStart.bind(this), false);
        document.addEventListener("touchmove", this.onMove.bind(this), false);
        document.addEventListener("touchend", this.onUp.bind(this), false);

        document.addEventListener("gesturechange", this.onGestureChange.bind(this), false);
    }

    originalEvent(e) {
        // jquery使用時用にoriginalEvent取得
        return (e = e.originalEvent ? e.originalEvent : e);
    }
    mouseDelta(e) {
        return e.deltaY ? -(e.deltaY) : e.wheelDelta ? e.wheelDelta : -(e.detail);
    }

    clientPosition(e) {
        this.originalEvent(e);
        if (e.touches) {
            return new Point(e.touches[0].clientX, e.touches[0].clientY);
        } else {
            return new Point(e.clientX, e.clientY);
        }
    }

    onStart(e) {
        this.originalEvent(e);
        this.isDown = true;
        this.diffPoint = new Point();
        this.startPoint = this.clientPosition(e);

        if (!e.touches) return;
        for (let i = e.touches.length - 1; i >= 0; i--) {
            this.touchesStartPoints[i] = new Point(e.touches[i].clientX, e.touches[i].clientY);
        }
    }

    onMove(e) {
        this.originalEvent(e);
        this.currentPoint = this.clientPosition(e);

        if (!this.isDown) return;
        this.diffPoint = new Point(this.currentPoint.x, this.currentPoint.y).subtract(this.startPoint);

        if (!e.touches) return;
        for (let i = e.touches.length - 1; i >= 0; i--) {
            this.touchesCurrentPoints[i] = new Point(e.touches[i].clientX, e.touches[i].clientY);
        }
    }

    onUp() {
        this.isDown = false;
    }

    onGestureChange(e) {
        this.originalEvent(e);
        let scale = 1;
        if (e.scale) {
            scale = e.scale;
        } else {
            const startDistance = Point.distance(this.touchesStartPoints[0], this.touchesStartPoints[1]);
            const currentDistance = Point.distance(this.touchesCurrentPoints[0], this.touchesCurrentPoints[1]);
            scale = currentDistance / startDistance;
        }
        this.scale = scale;
    }
}
// /* ===========================================================
// # Slider
// =========================================================== */
class Slider {
    constructor() {
        // -----------------------------
        this.slider = document.querySelector(".slider");
        this.slider__list = document.querySelector(".slider__list");
        this.slider__main = document.querySelector(".slider__main");
        this.slider__prev = document.querySelector(".slider__prev");
        this.slider__next = document.querySelector(".slider__next");
        this.slider__zoom = document.querySelector(".slider__zoom");
        this.slider__elm = document.querySelectorAll(".slider .slider__elm");
        // -----------------------------
        this.state = "ready";
        this.currentID = 1;
        // -----------------------------
        this.factor = 0.15; // イージング 係数
        this.fraction = 0.001; // スライド切り替え終点位置の繰り上げ・切り下げ 端数
        this.sliderWidth = 0; //スライド自体の幅
        this.responseRatio = 0.1; // mouse&touchmove時にスライドorもと位置に戻すかの判定
        // -----------------------------
        this.currentPoint = new Point(); // 現在位置
        this.targetPoint = new Point(); // 目標位置
        // -----------------------------
        this.resize();
        this.events();
        this.elements();
        this.update();
    }

    events() {
        this.slider__prev.addEventListener("click", () => {
            this.slide("prev");
        }, false);
        this.slider__next.addEventListener("click", () => {
            this.slide("next");
        }, false);
        this.slider__zoom.addEventListener("click", () => {
            Modal.instance.open(this.currentID);
        }, false);

        this.slider__main.addEventListener("mousemove", this.onMove.bind(this), false);
        this.slider__main.addEventListener("touchmove", this.onMove.bind(this), false);
        this.slider__main.addEventListener("mouseup", this.onUp.bind(this), false);
        this.slider__main.addEventListener("touchend", this.onUp.bind(this), false);

        window.addEventListener("resize", this.resize.bind(this), false);
    }

    resize() {
        this.sliderWidth = this.slider.clientWidth;
        Object.assign(this.slider__main.style, {
            transform: `translate(${-this.sliderWidth}px, 0px)`
        });
    }

    onMove() {
        if (this.state != "ready") return;
        if (!EventTouch.instance.isDown) return;
        this.targetPoint.x = EventTouch.instance.diffPoint.x;
    }

    onUp() {
        if (this.state != "ready") return;
        const ratio = Math.abs(EventTouch.instance.diffPoint.x) / this.sliderWidth;
        if (ratio < this.responseRatio) {
            this.targetPoint.x = 0;
            return;
        }
        const state = (Math.sign(EventTouch.instance.diffPoint.x) != 1) ? "next" : "prev";
        this.slide(state);
    }

    clear() {
        this.currentPoint = new Point();
        this.targetPoint = new Point();
        this.state = "ready";
    }

    elements() {
        const slider__elm = document.querySelectorAll(".slider .slider__elm");
        const firstNode = slider__elm[0];
        const lastNode = slider__elm[slider__elm.length - 1];

        if (this.state == "next") {
            this.slider__list.append(firstNode);
        } else {
            this.slider__list.insertBefore(lastNode, firstNode);
        }
    }

    slide(_state) {
        if (this.state != "ready") return;
        this.state = _state;

        if (_state == "prev") {
            this.targetPoint.x = this.sliderWidth;
            this.currentID--;
        }
        if (_state == "next") {
            this.targetPoint.x = -this.sliderWidth;
            this.currentID++;
        }

        if (this.currentID < 1) this.currentID = this.slider__elm.length;
        if (this.currentID > this.slider__elm.length) this.currentID = 1;
    }

    update() {
        window.requestAnimationFrame(this.update.bind(this));

        this.currentPoint.x += Number(this.targetPoint.x - this.currentPoint.x) * this.factor;
        const ratio = Math.abs(this.currentPoint.x) / this.sliderWidth;

        if (ratio > (1.0 - this.fraction)) {
            this.currentPoint.x = this.targetPoint.x;
            this.elements();
            this.clear();
        }
        if (ratio < this.fraction) {
            this.currentPoint.x = this.targetPoint.x = 0;
        }

        Object.assign(this.slider__list.style, {
            transform: `translate(${this.currentPoint.x}px, 0px)`
        });
    }
}
// /* ===========================================================
// # Modal
// =========================================================== */
class Modal {
    constructor() {
        if (Modal.instance) return;
        Modal.instance = this;
        // -----------------------------
        this.modal = document.querySelector(".modal");
        this.modal__close = document.querySelector(".modal__close");
        this.delay = 500;
        this.events();
    }

    events() {
        this.modal__close.addEventListener("click", () => {
            this.close();
        }, false);
    }

    open(_id) {
        const id = (`00${_id}`).slice(-2);
        Array.from(document.querySelectorAll(".zoom img"), (e) => {
            e.src = `https://nocebo.jp/data/zoomslider/img/${id}.jpg`;
        });
        this.modal.classList.add("active");
    }

    close() {
        this.modal.classList.remove("active");
        window.setTimeout(() => {
            // Zoom.instance.resetScale();
        }, this.delay);
    }
}
/* ===========================================================
# DOMContentLoaded
=========================================================== */
window.addEventListener("DOMContentLoaded", () => {
    new EventTouch();
    new Slider();
    new Modal();
}, false);

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.1/css/all.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.