A tutorial on how to create an experimental slideshow that animates in fragments. The slider is powered by the "Pieces" library, which was created for achieving interesting effects like these easily.

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:

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:

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:

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.

<!-- Pieces Slider --> <div class="pieces-slider"> <!-- Each slide with corresponding image and text --> <div class="pieces-slider__slide"> <img class="pieces-slider__image" src="img/ricardo-gomez-angel-381749.jpg" alt=""> <div class="pieces-slider__text">Ricardo Gomez Angel</div> </div> <div class="pieces-slider__slide"> <img class="pieces-slider__image" src="img/josh-calabrese-527813.jpg" alt=""> <div class="pieces-slider__text">Josh Calabrese</div> </div> <div class="pieces-slider__slide"> <img class="pieces-slider__image" src="img/samuel-zeller-103111.jpg" alt=""> <div class="pieces-slider__text">Samuel Zeller</div> </div> <div class="pieces-slider__slide"> <img class="pieces-slider__image" src="img/sweet-ice-cream-photography-143023.jpg" alt=""> <div class="pieces-slider__text">Sweet Ice Cream</div> </div> <div class="pieces-slider__slide"> <img class="pieces-slider__image" src="img/sticker-mule-199237.jpg" alt=""> <div class="pieces-slider__text">Sticker Mule</div> </div> <!-- Canvas to draw the pieces --> <canvas class="pieces-slider__canvas"></canvas> <!-- Slider buttons: prev and next --> <button class="pieces-slider__button pieces-slider__button--prev">prev</button> <button class="pieces-slider__button pieces-slider__button--next">next</button> </div>

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!