Last active
November 27, 2023 17:55
-
-
Save Kvaibhav01/108f9dce2a96718a357c1c8e4865c839 to your computer and use it in GitHub Desktop.
Layers Animation with clip-path and GSAP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*, | |
*::after, | |
*::before { | |
box-sizing: border-box; | |
} | |
:root { | |
font-size: 16px; | |
--color-text: #fafafb; | |
--color-bg: #0b0c0e; | |
--color-link: rgb(0 0 0 / 70%); | |
--color-link-hover: #fafafb; | |
--layer-width: 100vw; | |
--layer-height: 100vh; | |
--layer-radius: 0; | |
--page-padding: 1rem; | |
} | |
.demo-2 { | |
--color-bg: #94c3c1; | |
} | |
.demo-3 { | |
--layer-width: 98vw; | |
--layer-height: 96vh; | |
--layer-radius: 20px; | |
--color-bg: #f4b3b9; | |
} | |
.demo-4 { | |
--layer-width: 98vw; | |
--layer-height: 96vh; | |
--color-bg: #b3f4d7; | |
} | |
.demo-5 { | |
--color-bg: #d4b3f4; | |
} | |
.demo-6 { | |
--color-bg: #f4f3b3; | |
} | |
.demo-7 { | |
--color-bg: #f1b454; | |
--layer-width: 100vw; | |
--layer-height: 100vh; | |
} | |
body { | |
margin: 0; | |
color: var(--color-text); | |
background-color: var(--color-bg); | |
font-family: "Geist", -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, | |
Arial, sans-serif; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
width: 100%; | |
min-height: 100vh; | |
min-height: -webkit-fill-available; | |
overflow: hidden; | |
} | |
/* Page Loader */ | |
.js .loading::before, | |
.js .loading::after { | |
content: ""; | |
position: fixed; | |
z-index: 1000; | |
} | |
.js .loading::before { | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: var(--color-bg); | |
} | |
.js .loading::after { | |
top: 50%; | |
left: 50%; | |
width: 60px; | |
height: 60px; | |
margin: -30px 0 0 -30px; | |
border-radius: 50%; | |
opacity: 0.4; | |
background: var(--color-link); | |
animation: loaderAnim 0.7s linear infinite alternate forwards; | |
} | |
@keyframes loaderAnim { | |
to { | |
opacity: 1; | |
transform: scale3d(0.5, 0.5, 1); | |
} | |
} | |
a { | |
text-decoration: none; | |
color: var(--color-link); | |
outline: none; | |
cursor: pointer; | |
} | |
a:hover { | |
color: var(--color-link-hover); | |
outline: none; | |
} | |
/* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */ | |
a:focus { | |
/* Provide a fallback style for browsers | |
that don't support :focus-visible */ | |
outline: none; | |
background: lightgrey; | |
} | |
a:focus:not(:focus-visible) { | |
/* Remove the focus indicator on mouse-focus for browsers | |
that do support :focus-visible */ | |
background: transparent; | |
} | |
a:focus-visible { | |
/* Draw a very noticeable focus style for | |
keyboard-focus on browsers that do support | |
:focus-visible */ | |
outline: 2px solid red; | |
background: transparent; | |
} | |
main { | |
display: grid; | |
grid-template-areas: "main"; | |
width: 100%; | |
height: 100vh; | |
} | |
.unbutton { | |
background: none; | |
border: 0; | |
padding: 0; | |
margin: 0; | |
font: inherit; | |
cursor: pointer; | |
} | |
.unbutton:focus { | |
outline: none; | |
} | |
.frame { | |
grid-area: main; | |
position: relative; | |
padding: var(--page-padding); | |
display: grid; | |
z-index: 1000; | |
grid-template-columns: auto 1fr; | |
grid-template-areas: "title title" "prev back" "sponsor sponsor" "demos demos"; | |
grid-row-gap: 1rem; | |
grid-column-gap: 2rem; | |
pointer-events: none; | |
align-content: start; | |
} | |
.frame a { | |
pointer-events: auto; | |
} | |
.frame__title { | |
grid-area: title; | |
font-size: inherit; | |
margin: 0; | |
} | |
.frame__back { | |
grid-area: back; | |
justify-self: start; | |
} | |
.frame__prev { | |
grid-area: prev; | |
justify-self: start; | |
} | |
.frame__demos { | |
grid-area: demos; | |
position: relative; | |
overflow: hidden; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
align-self: start; | |
justify-self: start; | |
flex-wrap: wrap; | |
} | |
.frame__demos span { | |
margin-right: 1rem; | |
width: 100%; | |
} | |
.frame__demo { | |
width: 3rem; | |
aspect-ratio: 1; | |
height: auto; | |
display: block; | |
flex: none; | |
display: grid; | |
place-items: center; | |
} | |
.frame__demo--current, | |
.frame__demo--current:focus, | |
.frame__demo--current:hover { | |
background: var(--color-link-hover); | |
color: var(--color-bg); | |
} | |
body #cdawrap { | |
justify-self: start; | |
} | |
.content { | |
grid-area: main; | |
display: grid; | |
width: 100vw; | |
height: 100vh; | |
position: relative; | |
grid-template-areas: "content"; | |
} | |
.content__inner { | |
grid-area: content; | |
width: 100%; | |
display: grid; | |
grid-template-areas: "text" "headline"; | |
padding: 2rem var(--page-padding) var(--page-padding); | |
grid-template-rows: 1fr auto; | |
will-change: transform; | |
} | |
.hidden { | |
visibility: hidden; | |
pointer-events: none; | |
} | |
.content__inner h2 { | |
grid-area: headline; | |
font-size: clamp(2rem, 10vw, 13rem); | |
font-family: "PP Eiko", sans-serif; | |
text-transform: uppercase; | |
letter-spacing: 10px; | |
font-weight: 900; | |
margin: 0; | |
line-height: 1; | |
} | |
.content__inner p { | |
grid-area: text; | |
font-size: clamp(1rem, 3vw, 2.5rem); | |
line-height: 1.2; | |
font-weight: 300; | |
align-self: end; | |
} | |
.layers { | |
grid-area: content; | |
align-self: center; | |
justify-self: center; | |
flex: none; | |
position: relative; | |
width: var(--layer-width); | |
height: var(--layer-height); | |
} | |
.layers__item, | |
.layers__item-img { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
} | |
.layers__item { | |
overflow: hidden; | |
opacity: 0; | |
will-change: clip-path; | |
border-radius: var(--layer-radius); | |
} | |
.layers__item-img { | |
background-size: cover; | |
background-position: 50% 50%; | |
} | |
@media screen and (min-width: 53em) { | |
:root { | |
--page-padding: 3rem; | |
} | |
.frame { | |
height: 100vh; | |
width: 100%; | |
grid-template-columns: auto auto auto 1fr; | |
grid-template-rows: auto auto; | |
grid-template-areas: "title prev back sponsor" "demos demos demos demos"; | |
align-content: space-between; | |
justify-items: start; | |
grid-gap: 2rem; | |
} | |
.frame__demos { | |
padding-left: 2rem; | |
border: 1px solid; | |
border-radius: 2rem; | |
} | |
.frame__demos span { | |
width: auto; | |
} | |
.content { | |
height: 100vh; | |
} | |
.content__inner { | |
padding: 2rem var(--page-padding) calc(var(--page-padding) + 15vh); | |
} | |
main::before { | |
top: 4vh; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class Content { | |
// Class property initialization with default values. | |
// The DOM property holds references to the main and inner elements of the Content component. | |
DOM = { | |
el: null, // Holds the reference to the main DOM element with class 'content__inner'. | |
title: null, // Holds the reference to the inner DOM element <h2>. | |
description: null, // Holds the reference to the inner DOM element <p>. | |
}; | |
/** | |
* Constructor for the Content class. Initializes the instance, sets up DOM references, and binds events. | |
* @param {HTMLElement} DOM_el - The main DOM element for the Content | |
*/ | |
constructor(DOM_el) { | |
// Assign the provided DOM element to the 'el' property of the 'DOM' object. | |
this.DOM.el = DOM_el; | |
this.DOM.title = this.DOM.el.querySelector("h2"); | |
this.DOM.description = this.DOM.el.querySelector("p"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en" class="no-js"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<title>Layers Animation with Clip-path</title> | |
<link rel="stylesheet" type="text/css" href="css/base.css" /> | |
<script> | |
document.documentElement.className = "js"; | |
</script> | |
</head> | |
<body class="demo-1 loading"> | |
<main> | |
<div class="frame"></div> | |
<div class="content"> | |
<div class="content__inner"> | |
<h2>Lay→ers</h2> | |
<p> | |
Unlock the boundless potential of 3D tech and <br /> | |
animations. Click anywhere to begin! | |
</p> | |
</div> | |
<div class="content__inner hidden"> | |
<h2>sre←yal</h2> | |
<p> | |
You found a hidden layer! This was made possible <br /> | |
with CSS <code>clip-path</code> animations GSAP | |
</p> | |
</div> | |
<div class="layers"> | |
<div class="layers__item"> | |
<div | |
class="layers__item-img" | |
style="background-image: url(img/6.png)" | |
></div> | |
</div> | |
<div class="layers__item"> | |
<div | |
class="layers__item-img" | |
style="background-image: url(img/2.png)" | |
></div> | |
</div> | |
<div class="layers__item"> | |
<div | |
class="layers__item-img" | |
style="background-image: url(img/3.png)" | |
></div> | |
</div> | |
<div class="layers__item"> | |
<div | |
class="layers__item-img" | |
style="background-image: url(img/4.png)" | |
></div> | |
</div> | |
<div class="layers__item"> | |
<div | |
class="layers__item-img" | |
style="background-image: url(img/5.png)" | |
></div> | |
</div> | |
<div class="layers__item"> | |
<div | |
class="layers__item-img" | |
style="background-image: url(img/1.png)" | |
></div> | |
</div> | |
</div> | |
</div> | |
</main> | |
<script src="js/imagesloaded.pkgd.min.js"></script> | |
<script src="js/gsap.min.js"></script> | |
<script type="module" src="js/index.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Importing necessary functions and classes from other files | |
import { preloadImages } from "./utils.js"; // Utility function for preloading images | |
import { Item } from "./item.js"; // Item class | |
import { Content } from "./content.js"; // Content class | |
// frame element | |
const frameElement = document.querySelector(".frame"); | |
// Selecting all elements with class 'layers__item' and converting NodeList to an array | |
const DOMItems = [...document.querySelectorAll(".layers__item")]; | |
const items = []; // Array to store instances of the Item class | |
// Creating new instances of Item for each selected DOM element | |
DOMItems.forEach((item) => { | |
items.push(new Item(item)); // Initializing a new object for each item | |
}); | |
// Selecting all elements with class 'content__inner' and converting NodeList to an array | |
const DOMContentSections = [ | |
...document.querySelectorAll(".content > .content__inner"), | |
]; | |
const contents = []; // Array to store instances of the Content class | |
// Creating new instances of Content for each selected DOM element | |
DOMContentSections.forEach((content) => { | |
contents.push(new Content(content)); // Initializing a new object for each content | |
}); | |
// Toggle the "hidden" class between two content elements | |
const toggleContent = () => { | |
// Assuming there are only two content elements | |
const [content1, content2] = contents; | |
// Toggle the 'hidden' class on the first content element | |
if (content1.DOM.el.classList.contains("hidden")) { | |
content1.DOM.el.classList.remove("hidden"); | |
content2.DOM.el.classList.add("hidden"); | |
} else { | |
content1.DOM.el.classList.add("hidden"); | |
content2.DOM.el.classList.remove("hidden"); | |
} | |
}; | |
// GSAP timeline | |
let tl = null; | |
// Setting up the animation properties | |
const animationSettings = { | |
duration: 1.4, // Duration of the animation | |
ease: "power3.inOut", // Type of easing to use for the animation transition | |
delayFactor: 0.2, // Delay between each item's animation | |
}; | |
// Event listener for click events on the document | |
document.addEventListener("click", (event) => { | |
// Check if the timeline is currently active (running) | |
if ((tl && tl.isActive()) || frameElement.contains(event.target)) { | |
return false; // Don't start a new animation | |
} | |
// The currently active content element | |
const contentActive = contents.find( | |
(content) => !content.DOM.el.classList.contains("hidden") | |
); | |
// Assuming there are only two content elements | |
const contentInactive = contents.find((content) => content !== contentActive); | |
// Mapping each Item object to its actual DOM element for the animation | |
const allItems = items.map((item) => item.DOM.el); | |
// Isolating the last item's DOM element for a separate animation effect | |
const lastItem = items[items.length - 1].DOM.el; | |
// Mapping each Item object to its 'inner' property (inner image) | |
const allInnerItems = items.map((item) => item.DOM.inner); | |
// Creating a new GSAP timeline for managing a sequence of animations | |
tl = gsap | |
.timeline({ | |
paused: true, // Create the timeline in a paused state | |
defaults: { | |
// Default settings applied to all animations within this timeline | |
duration: animationSettings.duration, | |
ease: animationSettings.ease, | |
}, | |
}) | |
.fromTo( | |
allItems, | |
{ | |
// Initial animation state | |
opacity: 1, // Fully visible | |
"clip-path": "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)", // CSS clip-path shape | |
}, | |
{ | |
// Animation target state | |
stagger: { | |
// Settings for staggering animations for each item | |
each: animationSettings.delayFactor, // Time between each item's animation | |
onComplete: function () { | |
// Callback after each item finishes animating | |
const targetElement = this.targets()[0]; // The element that just finished animating | |
// Determining the index of the animated element within the original DOM NodeList | |
const index = DOMItems.indexOf(targetElement); | |
if (index) { | |
// If the element is not the first one (index 0) | |
// Set the opacity of the previous element to 0 | |
gsap.set(items[index - 1].DOM.el, { opacity: 0 }); | |
} | |
}, | |
}, | |
"clip-path": "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", // Target shape of the clip-path | |
}, | |
0 | |
) | |
.fromTo( | |
allInnerItems, | |
{ | |
// Starting state for 'inner' elements' animation | |
yPercent: 0, | |
filter: "brightness(30%)", // CSS filters to adjust color | |
}, | |
{ | |
// Animation target state | |
stagger: animationSettings.delayFactor, // Stagger settings similar to above | |
filter: "brightness(100%)", // Full brightness | |
}, | |
0 | |
) | |
.to( | |
[contentActive.DOM.title, contentActive.DOM.description], | |
{ | |
startAt: { yPercent: 0 }, | |
stagger: -0.04, | |
yPercent: -200, | |
}, | |
0 | |
) | |
.add(() => toggleContent()) | |
.fromTo( | |
[contentInactive.DOM.title, contentInactive.DOM.description], | |
{ | |
yPercent: 200, | |
}, | |
{ | |
ease: "expo", // Different easing effect | |
stagger: -0.15, | |
yPercent: 0, | |
} | |
) | |
.to( | |
lastItem, | |
{ | |
// Animation for the last item | |
duration: 1, | |
ease: "power4", // Different easing effect | |
"clip-path": "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)", // Animating clip-path to different shape | |
onComplete: () => gsap.set(lastItem, { opacity: 0 }), // After animation, hide the last item | |
}, | |
"<" | |
); | |
// Start the animation | |
tl.play(); | |
}); | |
// Preloading all images specified by the selector | |
preloadImages(".layers__item-img").then(() => { | |
// Once images are preloaded, remove the 'loading' indicator/class from the body | |
document.body.classList.remove("loading"); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class Item { | |
// Class property initialization with default values. | |
// The DOM property holds references to the main and inner elements of the Item component. | |
DOM = { | |
el: null, // Holds the reference to the main DOM element with class 'layers__item'. | |
inner: null, // Holds the reference to the inner DOM element with class 'layers__item-img'. | |
}; | |
/** | |
* Constructor for the Item class. Initializes the instance, sets up DOM references, and binds events. | |
* @param {HTMLElement} DOM_el - The main DOM element for the Item, expected to have a child with class 'layers__item-img'. | |
*/ | |
constructor(DOM_el) { | |
// Assign the provided DOM element to the 'el' property of the 'DOM' object. | |
this.DOM.el = DOM_el; | |
this.DOM.inner = this.DOM.el.querySelector(".layers__item-img"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Preloads images specified by the CSS selector. | |
* @function | |
* @param {string} [selector='img'] - CSS selector for target images. | |
* @returns {Promise} - Resolves when all specified images are loaded. | |
*/ | |
const preloadImages = (selector = "img") => { | |
return new Promise((resolve) => { | |
// The imagesLoaded library is used to ensure all images (including backgrounds) are fully loaded. | |
imagesLoaded( | |
document.querySelectorAll(selector), | |
{ background: true }, | |
resolve | |
); | |
}); | |
}; | |
// Exporting utility functions for use in other modules. | |
export { preloadImages }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Please make sure to import/install GSAP for this to work. Here's the demo:
https://twitter.com/vaibhav_khulbe/status/1729196795602112580
Thanks to Codrops for making the demo: https://tympanus.net/codrops/?p=74227