From our sponsor: Chromatic - Visual testing for Storybook, Playwright & Cypress. Catch UI bugs before your users do.
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.
Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.
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!
Very well constructed effect. Love to see reworks in the future a bit modest, less detailed versions of it!
Great overall job!
that’s amazing :O
nice paper
I love this effect! Very fresh and vivid! I am wondering if it’s possible to add a link on the tag? Thank you!
I meant on the “h2” tag.
It is not possible to add a link as you normally would. What you could do is insert a link on top of the canvas element, but you would have to position it manually.
Luรญs, thank you for the answer! I guess I can do it with this little jQuery help: http://jsfiddle.net/S79qp/
I won’t lie, I’m a little twisted this eve, but this effect physically affected my physiology.
This is really beautiful. From this I have created a react js that wraps the slider function. Here it is in the Codepen, https://codepen.io/mjunaidi/pen/qxemeJ
Speechless ………………
really awesome.
Thx alot for this tutorial. It’s a huge help to start learning canvas.
hey man, just checked out your website – it is real masterpiece in terms of animations. thanks for posting both article and link to website!
how do I leave the slids automatic without having to use buttons
Hi Luis you make a great great great tutorials.
Please can u check this website ‘https://antoni.de/’ and teach us how to make video slider like this ?
It would be great if you find some times to do that ๐ thanks again !
great video slider
+ here, if anyone find out, plz, do tell ๐
This css mastery is great! Brilliant design/effects!
This is what I have been waiting for. Respect this.
+ here, if anyone find out, plz, do tell ๐