Author Bhakti Al Akbar • April 23, 2017

Building Sliced Slideshow

Interactive slideshow with animated sliced segments

This exploration is heavily inspired by jetlag.photos. I will not explore 100% of the interaction, but I will adapt the sliced segments image, and use this feature to make a slideshow.

We’ll be using anime.js as the animation library and all images are taken from unsplash.

Here’s the markup for the slides

<!-- sections wrapper -->
<div id="sections-wrapper">
    <section class="show" data-img="img/mountain.jpg"></section>
    <section class="hide-bottom" data-img="img/shore.jpg"></section>
    <section class="hide-bottom" data-img="img/twilight.jpg"></section>
    <section class="hide-bottom" data-img="img/parachute.jpg"></section>
    <section class="hide-bottom" data-img="img/sky.jpg"></section>
</div>

CSS Style for the slides

#sections-wrapper {
    position: relative;
    overflow: hidden;
    height: 100vh;
    background: #000;
}

section {
    height: 100vh;
    font-size: 0;
    text-align: center;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
}

.hide-bottom {
    transform: translateY(100%);
}

.hide-top {
    transform: translateY(-100%);
}

As you can see from the above code, we wrap all slides into #sections-wrapper. We also use class show that indicates that our slide is currently visible. The .hide-bottom indicates that the slide is hidden on the bottom and .hidden-top is hidden on the top.

The sliced segments effect

Alright, here’s the interesting part. So originally the effect from jetlag.photos was created with HTML Canvas. But I wanted to it with CSS. So how does it work?

We generate a handful of segments (in our demo, it is 20 segments) and append them into the slide section. These segments are lined up so they look like a bunch of columns. We also set the width to be divided by 100 by the amount of segments.

Here’s the JavaScript code

// looping 20 times
for(var i = 0; i < segmentLength; i++){

    var segment = document.createElement("div");
    segment.className = "segment";

    // calculate the segment inner background position
    var posX = -i * 100/segmentLength;

    segment.innerHTML = "<div class='segment-inner' style='background-image: url(" + sectionImgURL + "); left: " + posX + "vw'></div>";

    // apply segment width by dividing 100 by 20
    segment.style.width = 100/segmentLength + "%";

    // append segment to section element
    fragment.appendChild(segment);
    section.appendChild(fragment);

}

Here’s the generated segments markup

image

On the segment element, we have a child element which is .segment-inner that contains the background image. We grab the section’s data-img as the background URL. The .segment-inner is also positioned absolute and has the same width and height as the sections-wrapper.

We move each .segment-inner by 5% from the left. The value increases if the segment index is higher.

Here’s the CSS style

.segment {
    height: 100%;
    display: inline-block;
    overflow: hidden;
    position: relative;
    box-sizing: border-box;
}

.segment-inner {
    background-image: url(img/twilight.jpg); /*added by JS*/
    left: -5vw; /*generated by JS*/
    position: absolute;
    top: 0;
    background: no-repeat center center / cover; 
    height: 100%;
    width: 100vw;
}

Adding User Interaction

We can interact with the slideshow using arrow keys (up, down, right, left) and the buttons. If the key is pressed, we will animate the already shown section and the hidden section.

var body = document.body;
var shownSection = body.querySelector(".show");
var animationSettings = {
    running: false
}

body.addEventListener("keyup", function(e){

    // if arrow down is clicked
    if(e.keyCode === 40 && shownSection.nextElementSibling && animationSettings.running === false) {

        animationSettings.running = true;
        animateSegments(shownSection.nextElementSibling, false);

    }

    // if arrow right is clicked
    if(e.keyCode === 39 && shownSection.nextElementSibling && animationSettings.running === false) {

        animationSettings.running = true;
        animateSegments(shownSection.nextElementSibling, false);

    }

    // if arrow up is clicked
    if(e.keyCode === 38 && shownSection.previousElementSibling && animationSettings.running === false) {

        animationSettings.running = true;
        animateSegments(shownSection.previousElementSibling, false);

    }

    // if arrow left is clicked
    if(e.keyCode === 37 && shownSection.previousElementSibling && animationSettings.running === false) {

        animationSettings.running = true;
        animateSegments(shownSection.previousElementSibling, false);

    }

});

var sectionThumbnails = document.querySelectorAll(".section-thumbnail"),

// if section thumbnail is clicked
sectionThumbnails.forEach(function(thumbnail){

    thumbnail.addEventListener("click", function(){

        if(animationSettings.running === false && !this.classList.contains("active")) {

            animationSettings.running = true;
            animateSegments(sections[sectionThumbnails.indexOf(this)], true);

        }
    })
});

The animateSegments function takes two arguments. First is the hidden section that we want to animate so it can be visible, and the second is basically a boolean variable that indicates whether the function is requested by click (using buttons) or arrow key.

// animate the section's segments
// first param is the next hidden section
// the second param defines whether the animation is requested by click event or not
function animateSegments(afterShownSection, byClickOrNot) {

    var translateYValue, // transition direction
            hiddenPosition; // hide at top or bottom

    if(afterShownSection.className === "hide-bottom") {

        translateYValue = "-100%";
        hiddenPosition = "hide-top";

    } else {

        translateYValue = "100%";
        hiddenPosition = "hide-bottom";

    }

    // get the index of after shown section
    var afterShownSectionIndex = sections.indexOf(afterShownSection);

    // shown section params
    var shownSectionParams = {
        // do function before animation starts
        begin: function(){

            // remove the active class from active thumbnails
            sectionThumbnails[sections.indexOf(shownSection)].classList.remove("active");

        },
        targets: shownSection.querySelectorAll(".segment"),
        complete: function(){

            this.animatables.forEach(function(animatable){
                animatable.target.style.transform = "translateY(0)";
            });

            // if the animate function is requested by click
            if(byClickOrNot) {

                sections.forEach(function(section, index){

                    // get all previous sections from the shown
                    if(index < afterShownSectionIndex) {
                        section.className = "hide-top";
                    }

                    // get all next sections from the shown
                    if(index > afterShownSectionIndex) {
                        section.className = "hide-bottom";
                    }

                });

            } 

            // if the animate function is requested by arrow key
            else {

                shownSection.className = hiddenPosition;

            }

        }
    }

    // after shown section params
    var afterShownParams = {
        begin: function(){

            sectionThumbnails[sections.indexOf(afterShownSection)].classList.add("active");

        },
        targets: afterShownSection.querySelectorAll(".segment"),
        complete: function(){

            this.animatables.forEach(function(animatable){
                animatable.target.style.transform = "translateY(0)";
            });

            afterShownSection.className = "show";

            shownSection = afterShownSection;

            animationSettings.running = false;

        }
    }

    // animate the shown section
    requestAnimate(shownSectionParams, translateYValue);

    // animate the hidden section
    requestAnimate(afterShownParams, translateYValue);

}

// utils

// request animate function
function requestAnimate(animationParams, translateYValue){

    anime({
        begin: animationParams.begin,
        targets: animationParams.targets,
        translateY: translateYValue,
        duration: animationSettings.duration,
        // each segment has higher delay depends on its index
        // the higher its index, the longer its delay
        delay: function(el, index) {
            return index * animationSettings.delay;
        },
        elasticity: animationSettings.elasticity,
        complete: animationParams.complete
    });

}

Final Words

I hope you find this exploration inspiring! If you want to play with the code yourself, feel free to check it out on GitHub.

comments powered by Disqus