Animated Fragment Slideshow

Animated Fragment Slideshow

Today we’d like to show you how to create a playful slider with an original fragmented look. Each element of the slider will be divided into pieces that will animate in different ways, using Pieces, a library that I’ve created for achieving interesting effects like these easily.

This will be the final result:

PiecesSlider

The animations are powered by anime.js.

The demo is kindly sponsored by: Northwestern’s Online Master’s in Information Design and Strategy.
If you would like to sponsor one of our demos, find out more here.

The original idea

The source of inspiration for this kind of effect came from the Dribbble shot Shift Animation by Alexander Saunki:

Shift Animation
Shift Animation

Since I saw it, I wanted to implement an effect like that for my new personal website, which I recently launched: lmgonzalves.com.

To achieve the desired effects, I developed a library that I called “Pieces”, since it allows to draw and animate text, images and SVG paths through rectangular pieces. So, without further ado, let’s see how to use this library!

Getting started with Pieces

All the detailed documentation about the Pieces library can be found in its Github repository. But anyway, let’s quickly see some essential concepts to be able to start using this library.

Assuming that we want to draw and animate an image, these are the basic elements that make up the scene:

Pieces Basic Elements
Pieces Basic Elements

As you can see, the image we want to draw will be our item, which will be divided into several pieces, which can also vary in size and position according to the options we define. To see all the possible options I recommend you check out the documentation on Github.

Throughout the tutorial we will explain each piece of code, so you can learn how to implement your own animations using the Pieces library. Let’s start!

HTML Structure

Before starting to write Javascript code, let’s see how we have defined the HTML for our slider. The markup is quite simple, since we have each slide with a corresponding image and text, the canvas element to animate things, and buttons to navigate through the slider.


Ricardo Gomez Angel
Josh Calabrese
Samuel Zeller
Sweet Ice Cream
Sticker Mule

Styling the slider

The slideshow needs some special styling for our effect. We’ll need to hide the images and texts as we’ll be redrawing them with our library. But we also want them to fallback to their initial markup if no JavaScript is available. Finally, we need to make sure that the slider is responsive with a couple of media queries:

.pieces-slider {
    position: relative;
    text-align: center;
    padding: 8rem 0;
}

.js .pieces-slider {
    padding: 0;
}

/* Make all slides absolutes and hide them */
.js .pieces-slider__slide {
    position: absolute;
    right: 100%;
}

/* Define image dimensions and also hide them */
.pieces-slider__image {
    max-width: 600px;
    max-height: 400px;
}

.js .pieces-slider__image {
    visibility: hidden;
}

/* Hide the titles */
.js .pieces-slider__text {
    text-indent: -9999px;
}

/* Canvas with viewport width and height */
.js .pieces-slider__canvas {
    position: relative;
    width: 100vw;
    height: 100vh;
    transition: 0.2s opacity;
}

/* Class for when we resize */
.pieces-slider__canvas--hidden {
    opacity: 0;
    transition-duration: 0.3s;
}

/* Navigation buttons */
.pieces-slider__button {
    position: absolute;
    left: 0;
    top: 50%;
    width: 100px;
    height: 100px;
    margin: -25px 0 0 0;
    background-color: #5104ab;
    color: #fff;
    font-family: inherit;
    font-weight: bold;
    border: none;
    cursor: pointer;
    transition: 0.1s background-color;
}

.pieces-slider__button:hover {
    background: #5f3abf;
}

.pieces-slider__button--next {
    left: auto;
    right: 0;
}

/* Hide the buttons when no JS */
.no-js .pieces-slider__button {
    display: none;
}

/* Media queries with styles for smaller screens */
@media screen and (max-width: 720px) {
    .pieces-slider__image {
        max-width: 300px;
    }
}

@media screen and (max-width: 55em) {
    .pieces-slider__canvas {
        width: 100vw;
        height: 100vw;
    }
    .pieces-slider__button {
        width: 60px;
        height: 60px;
    }
}

As you can see, we have hidden the HTML elements that we defined for our slider (except for the buttons), since we will draw everything in the canvas element.

Using Pieces to animate the slider

Let’s start by defining some variables and getting the slider info from the DOM:

// Get all images and texts, get the `canvas` element, and save slider length
var sliderCanvas = document.querySelector('.pieces-slider__canvas');
var imagesEl = [].slice.call(document.querySelectorAll('.pieces-slider__image'));
var textEl = [].slice.call(document.querySelectorAll('.pieces-slider__text'));
var slidesLength = imagesEl.length;

Then we need to define indexes variables to handle all the items we will draw on the canvas:

// Define indexes related variables, as we will use indexes to reference items
var currentIndex = 0, currentImageIndex, currentTextIndex, currentNumberIndex;
var textIndexes = [];
var numberIndexes = [];

// Update current indexes for image, text and number
function updateIndexes() {
    currentImageIndex = currentIndex * 3;
    currentTextIndex = currentImageIndex + 1;
    currentNumberIndex = currentImageIndex + 2;
}
updateIndexes();

Now we will start defining the options for each type of item (image, text, number and button). You can find a complete reference in the Pieces documentation, but here is a detailed explanation for each option used to draw the images:

// Options for images
var imageOptions = {
    angle: 45, // rotate item pieces 45deg
    extraSpacing: {extraX: 100, extraY: 200}, // this extra spacing is needed to cover all the item, because angle != 0
    piecesWidth: function() { return Pieces.random(50, 200); }, // every piece will have a random width between 50px and 200px
    ty: function() { return Pieces.random(-400, 400); } // every piece will be translated in the Y axis a random distance between -400px and 400px
};

In the same way, we will define the options for the other items types. Please see the comments to understand some of the properties used:

// Options for texts
var textOptions = {
    color: 'white',
    backgroundColor: '#0066CC',
    fontSize: function() { return windowWidth > 720 ? 50 : 30; },
    padding: '15 20 10 20',
    angle: -45,
    extraSpacing: {extraX: 0, extraY: 300},
    piecesWidth: function() { return Pieces.random(50, 200); },
    ty: function() { return Pieces.random(-200, 200); },
    translate: function() {
        if (windowWidth > 1120) return {translateX: 200, translateY: 200};
        if (windowWidth > 720) return {translateX: 0, translateY: 200};
        return {translateX: 0, translateY: 100};
    }
};

// Options for numbers
var numberOptions = {
    color: 'white',
    backgroundColor: '#0066CC',
    backgroundRadius: 300,
    fontSize: function() { return windowWidth > 720 ? 100 : 50; },
    padding: function() { return windowWidth > 720 ? '18 35 10 38' : '18 25 10 28'; },
    angle: 0,
    piecesSpacing: 2,
    extraSpacing: {extraX: 10, extraY: 10},
    piecesWidth: 35,
    ty: function() { return Pieces.random(-200, 200); },
    translate: function() {
        if (windowWidth > 1120) return {translateX: -340, translateY: -180};
        if (windowWidth > 720) return {translateX: -240, translateY: -180};
        return {translateX: -140, translateY: -100};
    }
};

Now we have all the options for each type of item, lets put them togheter to pass it to the Pieces library!

// Build the array of items to draw using Pieces
var items = [];
var imagesReady = 0;
for (var i = 0; i < slidesLength; i++) {
    // Wait for all images to load before initializing the slider and event listeners
    var slideImage = new Image();
    slideImage.onload = function() {
        if (++imagesReady == slidesLength) {
            initSlider();
            initEvents();
        }
    };
    // Push all elements for each slide with the corresponding options
    items.push({type: 'image', value: imagesEl[i], options: imageOptions});
    items.push({type: 'text', value: textEl[i].innerText, options: textOptions});
    items.push({type: 'text', value: i + 1, options: numberOptions});
    // Save indexes
    textIndexes.push(i * 3 + 1);
    numberIndexes.push(i * 3 + 2);
    // Set image src
    slideImage.src = imagesEl[i].src;
}

In addition to building the array of elements, in the previous code block we defined a simple mechanism to call the initSlider function only when all the images have been loaded. This is very important, since we will not be able to use Pieces to draw an image that is not available.

So far, we’ve not draw anything yet, but we’re ready to do that now. Let’s see how we are initializing a new instance of Pieces.

// Save the new Pieces instance
piecesSlider = new Pieces({
    canvas: sliderCanvas, // CSS selector to get the canvas
    items: items, // the Array of items we've built before
    x: 'centerAll', // center all items in the X axis
    y: 'centerAll', // center all items in the Y axis
    piecesSpacing: 1, // default spacing between pieces
    fontFamily: ["'Helvetica Neue', sans-serif"],
    animation: { // animation options to use in any operation
        duration: function() { return Pieces.random(1000, 2000); },
        easing: 'easeOutQuint'
    },
    debug: false // set `debug: true` to enable debug mode
});

And now, all the items and pieces are ready to be animated. They are being actually created, but are hidden by default, so, let’s see how to show the first slide and start the animations we want:

// Animate all numbers to rotate clockwise indefinitely
piecesSlider.animateItems({
    items: numberIndexes,
    duration: 20000,
    angle: 360,
    loop: true
});

// Show current items: image, text and number
showItems();

So, to show and hide the current items we need to call showItems and hideItems functions respectively. We have implemented them as follows:

// Show current items: image, text and number
function showItems() {
    // Show image pieces
    piecesSlider.showPieces({items: currentImageIndex, ignore: ['tx'], singly: true, update: (anim) => {
        // Stop the pieces animation at 60%, and run a new indefinitely animation of `ty` for each piece
        if (anim.progress > 60) {
            var piece = anim.animatables[0].target;
            var ty = piece.ty;
            anime.remove(piece);
            anime({
                targets: piece,
                ty: piece.h_ty < 300
                    ? [{value: ty + 10, duration: 1000}, {value: ty - 10, duration: 2000}, {value: ty, duration: 1000}]
                    : [{value: ty - 10, duration: 1000}, {value: ty + 10, duration: 2000}, {value: ty, duration: 1000}],
                duration: 2000,
                easing: 'linear',
                loop: true
            });
        }
    }});
    // Show pieces for text and number, using alternate `ty` values
    piecesSlider.showPieces({items: currentTextIndex});
    piecesSlider.showPieces({items: currentNumberIndex, ty: function(p, i) { return p.s_ty - [-3, 3][i % 2]; }});
}

// Hide current items: image, text and number
function hideItems() {
    piecesSlider.hidePieces({items: [currentImageIndex, currentTextIndex, currentNumberIndex]});
}

And finally to navigate through the slides, we’ve defined these functions:

// Select the prev slide: hide current items, update indexes, and show the new current item
function prevItem() {
    hideItems();
    currentIndex = currentIndex > 0 ? currentIndex - 1 : slidesLength - 1;
    updateIndexes();
    showItems();
}

// Select the next slide: hide current items, update indexes, and show the new current item
function nextItem() {
    hideItems();
    currentIndex = currentIndex < slidesLength - 1 ? currentIndex + 1 : 0;
    updateIndexes();
    showItems();
}

So we need to call those functions if the navigation buttons are clicked, or some of the arrow keys (left or right) are pressed:

// Select prev or next slide using buttons
prevButtonEl.addEventListener('click', prevSlide);
nextButtonEl.addEventListener('click', nextSlide);

// Select prev or next slide using arrow keys
document.addEventListener('keydown', function (e) {
    if (e.keyCode == 37) { // left
        prevSlide();
    } else if (e.keyCode == 39) { // right
        nextSlide();
    }
});

We are almost done 🙂 We just need to implement a responsive behavior, listening to the resize event, saving the current window width, and reinitializing the slider:

// Handle `resize` event
window.addEventListener('resize', resizeStart);

var initial = true, hideTimer, resizeTimer;

// User starts resizing, so wait 300 ms before reinitialize the slider
function resizeStart() {
    if (initial) {
        initial = false;
        if (hideTimer) clearTimeout(hideTimer);
        sliderCanvas.classList.add('pieces-slider__canvas--hidden');
    }
    if (resizeTimer) clearTimeout(resizeTimer);
    resizeTimer = setTimeout(resizeEnd, 300);
}

// User ends resizing, then reinitialize the slider
function resizeEnd() {
    initial = true;
    windowWidth = window.innerWidth;
    initSlider();
    hideTimer = setTimeout(() => {
        sliderCanvas.classList.remove('pieces-slider__canvas--hidden');
    }, 500);
}

And we are finally done!

We really hope this tutorial has been useful, and that you have found inspiration in these effects!

Animated Fragment Slideshow was written by Luis Manuel and published on Codrops.

Continue reading @tympanus

Leave a Reply

Close Menu