Writing a complicated but interesting JavaScript slider



Good day, friends! I decided to return to the topic of sliders. This article served as the inspiration . An article about the image gallery generator with a built-in slider is here .

The slider that we will write works on the principle of an endless card deck or a stack of images. The top image is discarded in one of three directions - left, right, or up. The discarded image is replaced by the following, and so on to infinity. You can upload your images or use the default images.

To register gestures (touches) and move (drag and drop) hammer.js is used . This library allows you to detect both mouse clicks and finger touches.

So let's go.

The markup looks like this:

<input type="file" multiple>
<button>build carousel</button>
<div></div>

We have an โ€œinputโ€ for loading images, a button for creating a carousel and a container for cards.

Add some styles:

body {
    margin: 0;
    overflow: hidden;
}

div {
    width: 100%;
    height: 100vh;
    position: relative;
}

img {
    width: 320px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(0.95);
    box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.1);
}

Nothing special here: the images are 320px wide, centered on top of one another.

We pass to JS. I will give the whole code. Do not be afraid of the number of lines, everything is quite simple, except, perhaps, calculating the positions and coordinates. Almost every line has a comment.

let button = document.querySelector('button')
let input = document.querySelector('input')
let files
let i = 0

button.addEventListener('click', () => {
    //   
    files = input.files

    document.body.removeChild(input)
    document.body.removeChild(button)

    let board = document.querySelector('div')
    //  
    let carousel = new Carousel(board)
})

class Carousel {
    constructor(element) {
        this.board = element

        //     
        this.push()

        //  
        i++
        this.push()

        //  
        this.handle()
    }

    handle() {
        //    
        this.cards = this.board.querySelectorAll('img')

        //   
        this.topCard = this.cards[this.cards.length - 1]

        //   
        this.nextCard = this.cards[this.cards.length - 2]

        //      
        if (this.cards.length > 0) {
            //      
            this.topCard.style.transform =
                'translate(-50%, -50%) rotate(0deg) scale(1)'

            //   ()   ()   
            this.hammer = new Hammer(this.topCard)
            this.hammer.add(new Hammer.Tap())
            this.hammer.add(new Hammer.Pan({
                position: Hammer.position_ALL,
                threshold: 0
            }))

            //        
            this.hammer.on('tap', (e) => {
                this.onTap(e)
            })
            this.hammer.on('pan', (e) => {
                this.onPan(e)
            })
        }
    }

    //  ()
    onTap(e) {
        //      
        let propX = (e.center.x - e.target.getBoundingClientRect().left) / e.target.clientWidth

        //      Y (+/-15 )
        let rotateY = 15 * (propX < 0.05 ? -1 : 1)

        //    transition
        this.topCard.style.transition = 'transform 100ms ease-out'

        // 
        this.topCard.style.transform =
            'translate(-50%, -50%) rotateX(0deg) rotateY(' + rotateY + 'deg) scale(1)'

        //   
        setTimeout(() => {
            //    transform
            this.topCard.style.transform =
                'translate(-50%, -50%) rotate(0deg) scale(1)'
        }, 100)

    }

    //  ()
    onPan(e) {
        if (!this.isPanning) {
            this.isPanning = true

            //    transition
            this.topCard.style.transition = null
            if (this.nextCard) this.nextCard.style.transition = null

            //      
            let style = window.getComputedStyle(this.topCard)
            let mx = style.transform.match(/^matrix\((.+)\)$/)
            this.startPosX = mx ? parseFloat(mx[1].split(', ')[4]) : 0
            this.startPosY = mx ? parseFloat(mx[1].split(', ')[5]) : 0

            //    
            let bounds = this.topCard.getBoundingClientRect()

            //      ,  (1)   (-1)
            this.isDraggingFrom =
                (e.center.y - bounds.top) > this.topCard.clientHeight / 2 ? -1 : 1
        }

        //   
        let posX = e.deltaX + this.startPosX
        let posY = e.deltaY + this.startPosY

        //       
        let propX = e.deltaX / this.board.clientWidth
        let propY = e.deltaY / this.board.clientHeight

        //   ,  (-1)   (1)
        let dirX = e.deltaX < 0 ? -1 : 1

        //   ,  0  +/-45 
        let deg = this.isDraggingFrom * dirX * Math.abs(propX) * 45

        //    ,  95  100%
        let scale = (95 + (5 * Math.abs(propX))) / 100

        //   
        this.topCard.style.transform =
            'translateX(' + posX + 'px) translateY(' + posY + 'px) rotate(' + deg + 'deg) scale(1)'

        //   
        if (this.nextCard) this.nextCard.style.transform =
            'translate(-50%, -50%) rotate(0deg) scale(' + scale + ')'

        if (e.isFinal) {
            this.isPanning = false

            let successful = false

            //    transition
            this.topCard.style.transition = 'transform 200ms ease-out'
            if (this.nextCard) this.nextCard.style.transition = 'transform 100ms linear'

            //  
            if (propX > 0.25 && e.direction == Hammer.DIRECTION_RIGHT) {
                successful = true

                //    
                posX = this.board.clientWidth

            } else if (propX < -0.25 && e.direction == Hammer.DIRECTION_LEFT) {
                successful = true

                //    
                posX = -(this.board.clientWidth + this.topCard.clientWidth)

            } else if (propY < -0.25 && e.direction == Hammer.DIRECTION_UP) {
                successful = true

                //    
                posY = -(this.board.clientHeight + this.topCard.clientHeight)

            }

            if (successful) {
                //     
                this.topCard.style.transform =
                    'translateX(' + posX + 'px) translateY(' + posY + 'px) rotate(' + deg + 'deg)'

                //   
                setTimeout(() => {
                    //   
                    this.board.removeChild(this.topCard)

                    //  
                    i++
                    //      ,  
                    if (i === files.length) i = 0

                    //   
                    this.push()
                    //      
                    this.handle()
                }, 200)

            } else {
                //   
                this.topCard.style.transform =
                    'translate(-50%, -50%) rotate(0deg) scale(1)'
                if (this.nextCard) this.nextCard.style.transform =
                    'translate(-50%, -50%) rotate(0deg) scale(0.95)'
            }
        }
    }

    //  
    push() {
        let card = document.createElement('img')
        
        //    ,    
        //  ,  
        if (files.length === 0) {
            card.src = 'https://picsum.photos/320/320/?random=' +
                Math.round(Math.random() * 1000000) +
                ')'
        } else {
            card.src = URL.createObjectURL(files[i])
        }

        if (this.board.firstChild) {
            this.board.insertBefore(card, this.board.firstChild)
        } else {
            this.board.append(card)
        }
    }
}



โ†’ Github

Thank you for your attention.

All Articles