Cube Slider Animation with GSAP and JavaScript
Recently, I was surfing the internet and looking at what interesting things other developers are making and sharing as open-source, hoping to find inspiration for my next tutorial. And I found many good examples, but one particularly impressed me. It is an animation, which I found on Codepen, called Hello From Dublin!, and created by the ingenious Pete Barr (you can find the link to the animation and the original design in the Credits section). Since I generally love sliders of all shapes and kinds, I thought... this could be made into a cool slider, and here we are...
As I already mentioned in the paragraph above, this time we will try to create a slider, which I have named Cube Slider because of the appearance of the section where the texts we will animate are located.
As usual, I first decided to quickly create a sketch that will give us an approximate idea of how it will all look.
In the sketch, you can see that the slider will consist of several parts, including the main central part where we will place, design, and animate our main "Hello from..." texts. Then there will be an additional smaller part that we will place to the left and right of the central div element, which we will also animate separately. The last but not insignificant part, which we will deal with later, and which you can also see in the sketch above, is the navigation that we will use to navigate through our slider.
So many words spent at the very beginning, and I haven't said anything useful yet. 🫣 Let's get straight to the point.
HTML Part
The HTML structure is designed in such a way that the central part of our tutorial is placed in one parent element, and the navigation is separated and placed in another parent element. Let's first look at the structure, and then below the following code, we will explain the entire structure in more detail.
Central section
1<div class="container">
2 <div class="hi" data-lat="51.5098° N" data-lang="-0.1180° E">
3 <div class="hi__cuboid">
4 <div class="face face--front">
5 <p class="hi__word">Hello</p>
6 </div>
7 <div class="face face--back">
8 <p class="hi__word">Hello</p>
9 </div>
10 <div class="face face--top">
11 <p class="hi__word">Hello</p>
12 </div>
13 <div class="face face--bottom">
14 <p class="hi__word">Hello</p>
15 </div>
16 </div>
17 <div class="hi__cuboid">
18 <div class="face face--front">
19 <p class="hi__word">From</p>
20 </div>
21 <div class="face face--back">
22 <p class="hi__word">From</p>
23 </div>
24 <div class="face face--top">
25 <p class="hi__word">From</p>
26 </div>
27 <div class="face face--bottom">
28 <p class="hi__word">From</p>
29 </div>
30 </div>
31 <div class="hi__cuboid">
32 <div class="face face--front">
33 <p class="hi__word">London</p>
34 </div>
35 <div class="face face--back">
36 <p class="hi__word">London</p>
37 </div>
38 <div class="face face--top">
39 <p class="hi__word">London</p>
40 </div>
41 <div class="face face--bottom">
42 <p class="hi__word">London</p>
43 </div>
44 </div>
45 </div>
46 <!-- ... other slides ... -->
47
48 <div class="hi__base">
49 <div class="hi__base-plate"></div>
50 <p class="hi__location hi__location--lat">53.3454° N</p>
51 <p class="hi__location hi__location--long">-6.3070° E</p>
52 </div>
53</div>
Our parent element with the class .container
will actually be the holder of our slider (slideshow) and will consist of two parts.
We will place our slides, which we will animate with a "cube" animation on click of the PREV or NEXT button, in the first part, a div element with the class .hi
. Each of these div elements will contain its main child element, specifically a child element with the class .hi__cuboid
, through which we will create our cube element.
On this .hi
element, we will place information about latitude (LAT) and longitude (LANG) that we will update each time the active slide changes. The information itself will be stored in data attributes data-lat and data-long.
Each cube element will consist of four child elements, each representing one side of our cube, designated in our code by the class .face
. However, each of these face elements will additionally have its unique class, which I want to list here in order: .face--front
, .face--back
, .face--top
, and .face--bottom
. We will design these so that together they form a cube in reality.
It's also important to note here that for each slide, we will have three words that we will animate simultaneously, each made in the way I explained a few seconds ago.
The second part located in this section is the element with the class .hi__base
. In this part, we will place additional text, specifically latitude (LAT) and longitude (LONG) information about the city whose slide is currently active. This part will not have a standard rotation animation; instead, we will create an additional character shuffle animation specifically for these two texts.
Navigation
1<div class="slider-controls">
2 <button class="slider-prev">Prev</button>
3 <button class="slider-next">Next</button>
4</div>
A very simple part and I think there's not much to explain here. In this element with the class .slider-controls
, we have two buttons that will navigate us through our slider. This part will get a bit more complicated in the JavaScript section, but don't worry... we've left all the challenging parts for the end. 😃
Styling Elements
As it usually happens, designing the application, website, and other elements tends to be almost the longest part of the project. However, since important things also happen during the design phase, I will try to break down this part into smaller sections and explain each of them in a few sentences. Nothing complicated happens in this phase, and anyone with basic CSS knowledge will be able to understand what is being discussed.
Custom Font Definition
1@font-face {
2 font-family: 'Inter Variable';
3 src: url(https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
4 unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
5 font-stretch: 100% 900%;
6 font-weight: 100 900;
7 font-display: swap;
8}
In this part, we define a custom font using the @font-face
rule. We specify the font family name, the source URL for the font file, and the character range that the font supports. Additionally, we set properties for the font's stretch, weight, and display. The inclusion of font-display: swap
secures that text remains visible during the font loading process.
Root Variables
1:root {
2 --grey: #5e5d5e;
3 --mid-grey: #3f3f3f;
4}
The :root
selector is where we define custom properties (variables) for colors that are applied across the entire CSS stylesheet. These variables are instrumental in maintaining and updating color values consistently throughout the design.
Box Sizing and Global Styles
1html {
2 box-sizing: border-box;
3}
4
5*, *:before, *:after {
6 box-sizing: inherit;
7}
8
9html, body {
10 width: 100%;
11 height: 100%;
12 margin: 0;
13 padding: 0;
14}
In our styling approach, we set box-sizing: border-box
universally across all elements, including *:before
and *:after
. This choice secures that padding and borders are included within each element's specified width and height, rather than adding to them. This method secures predictability in layout design, making it easier to manage spacing and alignment consistently throughout the application.
Additionally, we define the html
and body
elements to occupy the entire viewport without any default margin or padding.
Body Styles
1body {
2 display: flex;
3 align-items: center;
4 justify-content: center;
5 background-color: black;
6 font-family: 'Inter Variable';
7 color: white;
8 -webkit-font-smoothing: antialiased;
9 -moz-osx-font-smoothing: grayscale;
10 overflow: hidden;
11 background-size: 50px 50px;
12 background-position: center;
13 background-image: linear-gradient(to right, var(--mid-grey) 1px, transparent 1px), linear-gradient(to bottom, var(--mid-grey) 1px, transparent 1px);
14}
We configure the body
element to use our custom font and center its content using Flexbox. The background is set to black with a grid pattern for visual appeal. Font smoothing is applied to secure text clarity. Additionally, we use overflow: hidden
to prevent scrollbars from appearing, securing a clean and uninterrupted user interface.
Container Styles
1.container {
2 position: relative;
3 width: 100%;
4 height: 100%;
5 display: flex;
6 align-items: center;
7 justify-content: center;
8 perspective: 900px;
9 visibility: hidden;
10}
We use the .container
class to create a flex container that centers its child elements and applies a 3D perspective effect. The visibility: hidden
property is typically used to hide elements initially, revealing them later through animations or interactions.
Hi Class and Cuboid Styles
1.hi {
2 position: relative;
3 z-index: 1;
4 font-size: 100px;
5 font-stretch: 400%;
6 font-weight: 100;
7 line-height: 1;
8 text-transform: uppercase;
9 text-align: center;
10 transform-style: preserve-3d;
11}
12
13.hi__cuboid {
14 position: relative;
15 width: 500px;
16 height: 70px;
17 transform-style: preserve-3d;
18 margin: 30px 0;
19 display: none;
20 opacity: 0;
21}
We style the .hi
class to handle the main greeting text. It positions the text relatively, securing a high z-index for layering, and applies large font properties for visibility. The .hi__cuboid
class is responsible for styling cuboid elements that contain text faces. It arranges these faces in 3D space using transform-style: preserve-3d
, maintaining their 3D positioning for visual effects.
Cuboid Faces
1.hi__cuboid .face {
2 position: absolute;
3 left: 0;
4 top: 0;
5 background-color: black;
6}
7
8.hi__cuboid .face--front,
9.hi__cuboid .face--back,
10.hi__cuboid .face--top,
11.hi__cuboid .face--bottom {
12 width: 500px;
13 height: 70px;
14}
15
16.hi__cuboid .face--front {
17 transform: translateZ(calc(70px / 2));
18}
19
20.hi__cuboid .face--back {
21 transform: translateZ(calc(70px / 2 * -1)) rotateY(180deg) rotate(180deg);
22}
23
24.hi__cuboid .face--left {
25 width: 70px;
26 height: 70px;
27 transform: translateX(calc(70px / 2 * -1)) rotateY(-90deg);
28}
29
30.hi__cuboid .face--right {
31 width: 70px;
32 height: 70px;
33 transform: translateX(calc(500px - 70px / 2)) rotateY(90deg);
34}
35
36.hi__cuboid .face--top {
37 transform: translateY(calc(70px / 2 * -1)) rotateX(90deg);
38}
39
40.hi__cuboid .face--bottom {
41 transform: translateY(calc(70px - 70px / 2)) rotateX(-90deg);
42}
43
44.hi__cuboid.active {
45 display: block;
46 opacity: 1;
47}
In this section, we define the size and position of each side of the cuboid in 3D space. Each side (front, back, top, bottom) is precisely positioned and transformed using CSS. This secures that the cuboid elements are arranged correctly and interact effectively within the 3D space defined by the CSS transformations.
Face Content Styles
1.face {
2 display: flex;
3 align-items: center;
4 justify-content: center;
5 overflow: hidden;
6}
7
8.face.face--top,
9.face.face--bottom {
10 background: white;
11 color: black;
12}
In this section, the .face
class secures that its content is centered using Flexbox. Additional styles are applied to specific faces like the top and bottom faces to set their background and text colors.
Word and Base Styles
1.hi__word {
2 margin: 0;
3 transform: translateY(0);
4}
5
6.hi__base {
7 position: absolute;
8 z-index: 0;
9 top: 50%;
10 left: 50%;
11 transform: translate(-50%, -50%);
12 width: calc(100% - 3rem);
13 max-width: 752px;
14 height: 250px;
15}
16
17.hi__base-plate {
18 width: 100%;
19 height: 100%;
20 background: black;
21 border: 1px solid var(--grey);
22}
23
24.hi__location {
25 position: absolute;
26 margin: 0;
27 font-size: 20px;
28 font-stretch: 400%;
29 font-weight: 400;
30 min-width: 126px;
31 text-align: center;
32}
33
34.hi__location--lat {
35 top: 50%;
36 left: 0vw;
37 transform: rotate(-90deg) translateX(10px);
38}
39
40.hi__location--long {
41 top: 50%;
42 right: 0vw;
43 transform: rotate(90deg) translateX(-10px);
44}
We manage the appearance of text inside the cuboids with the .hi__word
class, securing it fits and looks appropriate within the 3D elements. The .hi__base
class is responsible for laying out and styling the base plate and displaying location data effectively.
Home Logo and Collection Styles
1.home-logo {
2 width: 20vw;
3 max-width: 150px;
4 position: fixed;
5 bottom: 15px;
6 right: 15px;
7 color: white;
8 text-align: right;
9}
10
11.collection {
12 position: fixed;
13 z-index: 1000;
14 top: 24px;
15 right: 24px;
16 display: flex;
17 flex-direction: column;
18}
19
20.collection.collection__bottom {
21 top: inherit;
22 bottom: 24px;
23}
24
25.collection__link {
26 position: relative;
27 margin-bottom: 16px;
28 color: white;
29 text-decoration: none;
30 font-size: 16px;
31}
32
33.collection__link span {
34 display: block;
35 position: absolute;
36 bottom: -3px;
37 left: 0;
38 height: 1px;
39 width: 10%;
40 background-color: white;
41 content: "";
42 transition: width 0.3s;
43}
44
45.collection__link:hover {
46 text-decoration: none;
47}
48
49.collection__link:hover span {
50 width: 100%;
51}
We style a fixed-position logo at the bottom right with the .home-logo
class. Additionally, the .collection
class positions a fixed element for navigation links, styles the links, and adds hover effects. This part is not important at all in our tutorial, but it's here, so as not to confuse you.
Slider Controls Styling
1.slider-controls {
2 position: fixed;
3 bottom: 50%;
4 transform: translateY(50%);
5 display: flex;
6 justify-content: space-between;
7 width: 100%;
8 padding: 0 20px;
9}
We use the .slider-controls
class to style fixed-position navigation buttons located at the bottom of the viewport. This class utilizes flexbox (display: flex;
) to evenly distribute the buttons (justify-content: space-between;
). Adding padding: 0 20px;
creates space around the buttons for better spacing and alignment.
Button Styling
1.slider-controls button {
2 background: none;
3 border: 1px solid white;
4 color: white;
5 padding: 10px 20px;
6 cursor: pointer;
7}
We style the navigation buttons using the .slider-controls button
selector. These buttons are designed with a transparent background, a white border, white text for visibility, padding to provide spacing around the text, and a pointer cursor to indicate interactivity.
Media Query for Responsive Design
1@media screen and (max-width: 960px) {
2 .container {
3 flex-direction: column;
4 }
5
6 .hi {
7 font-size: 60px;
8 }
9
10 .hi__cuboid {
11 width: 300px;
12 height: 50px;
13 margin: 75px 0;
14 }
15
16 .hi__base {
17 height: 520px;
18 }
19
20 .slider-controls {
21 width: auto;
22 bottom: 75px;
23 transform: translateY(0);
24 }
25
26 .slider-controls button {
27 margin: 0 0.325rem;
28 }
29}
At @media screen and (max-width: 960px)
, we adjust our layout for smaller screens. We make .container
switch to a column layout, reduce the font size of .hi
, resize .hi__cuboid
dimensions, adjust the height of .hi__base
, and reposition .slider-controls
to fit within the viewport.
JavaScript Code Breakdown
In this part, we will add all functionalities and animations to our static, nicely designed slider (slideshow). For animations, we will once again use the excellent GreenSock (GSAP) library. The JavaScript part will be divided into several functions, and we will explain each of them in detail in the following section.
If you are already familiar with GSAP, you will have no trouble following this part of the tutorial. However, if you are new to programming and don't know what GreenSock (GSAP) actually is, I recommend scrolling to the end of the page and finding the link in the Credits that leads to their website. Take a look at their Documents page.
Variable Initialization
Here we will initialize the application by setting up various variables and selecting necessary DOM elements using helper functions (select
and selectAll
). These functions streamline the process of accessing elements from the HTML document, making our code more concise and readable.
Next, we will initialize the application state and dimensions. This includes setting initial values for variables such as container
, hiElements
, baseLat
, baseLang
, prevButton
, nextButton
, winW
, winH
, pointer
, currentIndex
, isAnimating
, and rotationTimeline
. These variables are crucial for managing the application's state, tracking dimensions, and handling user interaction.
1console.clear();
2
3const select = e => document.querySelector(e);
4const selectAll = e => document.querySelectorAll(e);
5
6const container = select('.container');
7const hiElements = selectAll('.hi');
8const hiWords = selectAll('.hi__word');
9const baseLat = select('.hi__location--lat');
10const baseLang = select('.hi__location--long');
11const prevButton = select('.slider-prev');
12const nextButton = select('.slider-next');
13let winW = 0;
14let winH = 0;
15let pointer = {
16 x: window.innerWidth / 2,
17 y: window.innerHeight / 2
18};
19let currentIndex = 0;
20let isAnimating = false;
21
22let rotationTimeline;
Then, we will define the init()
function. This function prepares the application for display and animation.
Initialization Functions
1function init() {
2 setWinDimensions();
3 gsap.set(container, {
4 autoAlpha: 1
5 });
6
7 const initialHi = hiElements[currentIndex];
8 const initialCuboids = initialHi.querySelectorAll('.hi__cuboid');
9
10 gsap.set(initialCuboids, {
11 display: 'block',
12 autoAlpha: 0
13 });
14
15 gsap.timeline({
16 delay: 0.5
17 })
18 .from('.hi__location--lat', {
19 x: 100,
20 autoAlpha: 0,
21 ease: 'power4',
22 duration: 1
23 })
24 .from('.hi__location--long', {
25 x: -100,
26 autoAlpha: 0,
27 ease: 'power4',
28 duration: 1
29 }, 0)
30 .from(initialCuboids, {
31 y: winH,
32 duration: 3,
33 stagger: 0.14,
34 ease: 'elastic(0.4,0.3)'
35 }, 0)
36 .to(initialCuboids, {
37 autoAlpha: 1,
38 duration: 0,
39 stagger: 0
40 }, 0);
41
42 startRotation();
43}
In this function, we initialize the application by first setting the initial window dimensions using setWinDimensions()
. This secures that the application is responsive and adapts correctly to the user's screen size. Next, we fade in the main container (container
), progressively making it visible to the user.
Following that, we animate key elements such as hi__location--lat
, hi__location--long
, and hi__cuboid
using GSAP timelines. These animations are carefully sequenced with a slight delay (delay: 0.5
) and employ easing functions (ease: 'power4'
) to achieve smooth and attractive transitions.
Lastly, init()
initiates the rotation animation (startRotation()
), which continuously rotates elements within the application. This animated rotation adds dynamism and secures the interactive nature of the application.
1function startRotation() {
2 if (rotationTimeline) {
3 rotationTimeline.kill(); // Kill existing timeline if it exists
4 }
5
6 rotationTimeline = gsap.timeline({
7 repeat: -1
8 });
9
10 rotationTimeline.to('.hi__cuboid', {
11 rotateX: '+=360',
12 duration: 8,
13 ease: 'none',
14 yoyo: true,
15 repeat: -1,
16 });
17
18 rotationTimeline.fromTo('.hi__cuboid', {
19 rotateY: 8,
20 rotate: -10
21 }, {
22 rotateY: -8,
23 rotate: 10,
24 duration: 2.2,
25 yoyo: true,
26 repeat: -1,
27 ease: 'sine.inOut'
28 }, 0);
29}
In this part, we set up a GSAP timeline (rotationTimeline
) dedicated to animating continuous rotations of .hi__cuboid
elements within the application. This timeline is important for keeping an exciting look that makes users more interested and involved.
The timeline defines rotations along both the X and Y axes, securing that the elements rotate smoothly and continuously. This motion is configured with specific durations and easing functions tailored to achieve the desired visual appeal and smoothness. By using GSAP's features, we secure that the rotation animation is smooth and adds an exciting element to how users experience everything.
1function setWinDimensions() {
2 winW = window.innerWidth;
3 winH = window.innerHeight;
4}
In this part, we maintain the accuracy and responsiveness of animations and calculations that rely on window dimensions (winW
and winH
). The function updates winW
and winH
whenever the window is resized, ensuring that these variables accurately reflect the current dimensions of the browser window.
By dynamically adjusting winW
and winH
based on window resizing events, the application can adapt smoothly to changes in screen size. This responsiveness secures that animations and layout calculations remain precise and looks good on different devices and screen sizes, making the user experience better with smooth and consistent interactions.
Calculation and Animation Functions
1function calcOffset(xPos, yPos) {
2 let dX = 2 * (xPos - winW / 2) / winW;
3 let dY = -2 * (yPos - winH / 2) / winH;
4 return [dX, dY];
5}
We compute the offset of the cursor (pX
, pY
) relative to the center of the screen (winW
and winH
). This involves calculating dX
and dY
values derived from the cursor's position, which indicate how far the cursor is from the center along the horizontal and vertical axes.
1function followPointer(pX, pY) {
2 let nPos = calcOffset(pX, pY); // get cursor position from center
3 let nY = nPos[1];
4 let positiveY = Math.sqrt(nY*nY);
5 let deltaW = 600*positiveY;
6
7 gsap.to(hiWords, {
8 fontWeight: 900-deltaW,
9 duration: 0.75
10 });
11}
We adjust the CSS property (fontWeight
) of hiWords
based on the cursor position (pX
, pY
). Calculating deltaW
involves assessing the vertical cursor position relative to the screen center (positiveY
).
1function shuffleText(element, newText) {
2 const originalText = element.textContent;
3 const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
4 let shuffledText = '';
5 let iterations = 0;
6 const shuffleInterval = setInterval(() => {
7 // Generate shuffled text with original characters in random order
8 shuffledText = originalText.split('').sort(() => Math.random() - 0.5).join('');
9 element.textContent = shuffledText;
10 if (iterations > originalText.length * 2) {
11 clearInterval(shuffleInterval);
12 element.textContent = newText;
13 }
14 iterations++;
15 }, 25);
16}
We animate the shuffling effect of text for specified elements (element
). This function generates a randomized version of the original text (originalText
) by sorting characters randomly and smoothly transitions it to display newText
.
Slide Navigation and Event Handling
1function changeSlide(next = true) {
2 if (isAnimating) return;
3 isAnimating = true;
4
5 const oldIndex = currentIndex;
6 currentIndex = next ? (currentIndex + 1) % hiElements.length : (currentIndex - 1 + hiElements.length) % hiElements.length;
7
8 const oldHi = hiElements[oldIndex];
9 const newHi = hiElements[currentIndex];
10
11 const oldCuboids = oldHi.querySelectorAll('.hi__cuboid');
12 const newCuboids = newHi.querySelectorAll('.hi__cuboid');
13
14 const newLat = newHi.getAttribute('data-lat');
15 const newLang = newHi.getAttribute('data-lang');
16
17 // Kill previous rotation animation to avoid resetting the state
18 rotationTimeline.kill();
19
20 gsap.set(oldCuboids, {
21 display: 'none',
22 autoAlpha: 0
23 });
24
25 gsap.set(newCuboids, {
26 display: 'block',
27 autoAlpha: 0
28 });
29
30 gsap.timeline().to(newCuboids, {
31 autoAlpha: 1,
32 duration: 0,
33 stagger: 0,
34 });
35
36 gsap.timeline().to(newCuboids, {
37 rotateX: '+=360 * 4',
38 rotateY: 8,
39 rotate: -10,
40 duration: 1.75,
41 ease: 'elastic(0.4,0.3)',
42 stagger: 0,
43 onComplete: () => {
44 shuffleText(baseLat, newLat);
45 shuffleText(baseLang, newLang);
46 isAnimating = false;
47 // Restart the rotation animation after slide change
48 startRotation();
49 }
50 });
51}
We will handle navigation between slides (hiElements
) based on the next
parameter. This function manages the animation state (isAnimating
), updates the current slide index (currentIndex
), and orchestrates transitions between the old and new slides using GSAP animations. This ensures smooth visual changes by fading out the previous slide (oldCuboids
) and fading in the new slide (newCuboids
).
Additionally, we will rotate the new slide (newCuboids
) along multiple axes (rotateX
, rotateY
, rotate
) for a duration of 1.75
seconds with an elastic easing effect (elastic(0.4,0.3)
). After completing the slide transition, we will animate and update textual content (baseLat
and baseLang
) using shuffleText
.
Once all animations are finished, we will reset the isAnimating
flag and restart the rotation animation (startRotation()
), ensuring a seamless user experience throughout slide navigation.
Event Listeners and Initialization
We will add event listeners (mousemove
, touchmove
, touchstart
, click
) to track user interactions such as cursor movements, touch events, and button clicks.
1window.addEventListener("mousemove", function(event) {
2 pointer.x = event.clientX;
3 pointer.y = event.clientY;
4 followPointer(pointer.x, pointer.y);
5});
6
7window.addEventListener('touchmove', function(event) {
8 pointer.x = event.touches[0].clientX;
9 pointer.y = event.touches[0].clientY;
10 followPointer(pointer.x, pointer.y);
11});
12
13window.addEventListener('touchstart', function(event) {
14 pointer.x = event.touches[0].clientX;
15 pointer.y = event.touches[0].clientY;
16 followPointer(pointer.x, pointer.y);
17});
18
19prevButton.addEventListener('click', () => changeSlide(false));
20nextButton.addEventListener('click', () => changeSlide(true));
21
22window.onload = () => {
23 init();
24};
These listeners will trigger corresponding functions (followPointer
, changeSlide
) to handle user input and update the application's state accordingly. Additionally, the window.onload
event will guarantee that the init()
function executes once all resources are loaded, initializing the demo and start animations.
And that's it! I hope you've enjoyed it and that this will serve as a foundation for your next project or inspire you for an even better one. Enjoy, share the article with friends, and visit us again.