Skip to content

Instantly share code, notes, and snippets.

@Kvaibhav01
Last active November 27, 2023 17:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kvaibhav01/108f9dce2a96718a357c1c8e4865c839 to your computer and use it in GitHub Desktop.
Save Kvaibhav01/108f9dce2a96718a357c1c8e4865c839 to your computer and use it in GitHub Desktop.
Layers Animation with clip-path and GSAP
*,
*::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;
}
}
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");
}
}
<!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>
// 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");
});
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");
}
}
/**
* 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 };
@Kvaibhav01
Copy link
Author

Kvaibhav01 commented Nov 27, 2023

Please make sure to import/install GSAP for this to work. Here's the demo:

Tweet screenshot
https://twitter.com/vaibhav_khulbe/status/1729196795602112580

Thanks to Codrops for making the demo: https://tympanus.net/codrops/?p=74227

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment