Celestial Cart: Crafting an Enchanting Shopping Experience with Physics-Based Aura Animations "Celestial Cart - Aura Pull" aims to create a truly enchanting and unique user experience for an e-commerce platform. This iteration focuses on an ethereal and luminous design language, where interactions feel organic and almost magical. The visual palette is composed of soft, celestial whites and creams, accented by a flowing gradient of pale sky blues, gentle lavenders, and a touch of pearlescent shimmer, evoking a serene and dreamlike atmosphere. Typography is carefully chosen, with the elegant serif "Cormorant Garamond" for headings and the clean, light "Montserrat" for body text, enhancing the delicate and sophisticated feel.
The signature interaction of this design is the "Magnetic Aura Pull." Upon adding an item to the bag, a soft, pulsating aura of light—representing the product—appears near the "Add to Bag" button. This aura doesn't immediately fly to the cart. Instead, if the user's mouse pointer moves into the vicinity of the main navigation cart icon, the aura begins to drift towards it, as if guided by a gentle, unseen force. This movement is not a direct animation path but is simulated using a simple physics engine within JavaScript, managed by GSAP for smoothness. The aura accelerates subtly as it gets closer to the cart icon, creating a natural and engaging "pull" effect. The cart icon itself features a soft, expanding light ripple effect on hover, visually indicating its "magnetic field."
The cart panel, a crucial component, slides in smoothly from the right, maintaining the ethereal aesthetic with its clean lines, spacious layout, and soft color palette. The process of rendering items within this panel has been meticulously refined to prevent any layout shifts on the main page. Adding, removing, or changing the quantity of items in the cart triggers fluid GSAP animations within the panel, ensuring a seamless user experience. The "empty cart" state is also elegantly handled. Page load animations, powered by AOS, introduce product cards with graceful, varied transitions, contributing to the overall polished and high-quality feel of the interface. This project demonstrates how physics-based interactions and a strong thematic design, orchestrated by GSAP, can create a deeply engaging and memorable e-commerce journey.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Celestial Cart - Aura Pull (Fixed)</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Montserrat:wght@300;400;500;600&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet" />
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header
data-aos="fade-in"
data-aos-duration="1200"
data-aos-easing="ease-out-cubic"
>
<div class="container">
<nav>
<div class="logo">
<i class="fas fa-cloud-moon"></i>
<span>Celestial Cart</span>
</div>
<div class="nav-cart-icon-wrapper" id="navCartIconWrapper">
<i class="fas fa-shopping-basket nav-cart-icon"></i>
<span class="nav-cart-count" id="navCartCount">0</span>
</div>
</nav>
</div>
</header>
<main class="container">
<div class="products-grid" id="productsGrid">
<!-- Products -->
</div>
</main>
<div class="cart-overlay" id="cartOverlay"></div>
<div class="cart-panel" id="cartPanel">
<div class="cart-panel__header">
<div class="cart-panel__title">
<i class="fas fa-star-shooting"></i><span>Constellation</span>
</div>
<button class="cart-panel__close-btn" id="closeCartBtn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="cart-panel__content-area">
<!-- New Wrapper -->
<div id="cartItemsContainer">
<!-- Cart items will be dynamically added here -->
</div>
<div id="emptyCartMessage" class="cart-panel__empty-cart">
<!-- Start visible if cart is empty -->
<i class="far fa-moon"></i>
<p>Your constellation awaits.</p>
<p>Gather starlight!</p>
</div>
</div>
<div class="cart-panel__footer">
<div class="cart-panel__total">
<span>Subtotal:</span
><span class="cart-panel__total-amount" id="cartTotalAmount"
>$0.00</span
>
</div>
<button class="checkout-btn" id="checkoutBtn" disabled>
<i class="fas fa-meteor"></i><span>Launch Expedition</span>
</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
:root {
--bg-celestial-white: #fcfcff;
--bg-card: #ffffff;
--bg-panel: #ffffff;
--text-deep-sky: #3a4d6f;
--text-soft-sky: #6b7f9d;
--text-whisper: #a8b6c9;
--accent-moonbeam-gold: #d4af37;
--accent-starlight-silver: #c0c0c0;
--accent-aura-lavender: #d8bfd8;
--accent-aura-blue: #b0e0e6;
--aura-gradient: linear-gradient(
135deg,
rgba(176, 224, 230, 0.7) 0%,
rgba(216, 191, 216, 0.7) 50%,
rgba(255, 228, 196, 0.7) 100%
);
--glow-aura: 0 0 20px var(--accent-aura-lavender),
0 0 35px var(--accent-aura-blue);
--glow-subtle-gold: 0 0 15px rgba(212, 175, 55, 0.3);
--success-celestial: #a6d7ae;
--border-ethereal: #f0f2f7;
--shadow-celestial: 0 15px 40px rgba(107, 127, 157, 0.08);
--shadow-interactive-light: 0 8px 25px rgba(212, 175, 55, 0.15);
--radius-celestial: 22px;
--radius-button-soft: 30px;
--font-heading: "Cormorant Garamond", serif;
--font-body: "Montserrat", sans-serif;
--transition-celestial: 0.65s cubic-bezier(0.23, 1, 0.32, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-body);
background-color: var(--bg-celestial-white);
color: var(--text-deep-sky);
line-height: 1.75;
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
max-width: 1300px;
margin: 0 auto;
padding: 0 2rem;
}
header {
background-color: rgba(252, 252, 255, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 3px 18px rgba(107, 127, 157, 0.05);
position: sticky;
top: 0;
z-index: 1000;
padding: 1.2rem 0;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-family: var(--font-heading);
font-size: 2.4rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.7rem;
color: var(--text-deep-sky);
letter-spacing: 0.2px;
}
.logo i {
color: var(--accent-moonbeam-gold);
font-size: 1.8rem;
filter: drop-shadow(var(--glow-subtle-gold));
}
.nav-cart-icon-wrapper {
position: relative;
cursor: pointer;
padding: 15px;
border-radius: 50%;
transition: background-color 0.5s ease;
}
.nav-cart-icon-wrapper:hover {
background-color: rgba(216, 191, 216, 0.08);
}
.nav-cart-icon {
font-size: 1.8rem;
color: var(--text-soft-sky);
transition: color 0.5s ease, transform 0.5s var(--transition-celestial),
filter 0.5s ease;
}
.nav-cart-icon-wrapper:hover .nav-cart-icon {
color: var(--accent-moonbeam-gold);
transform: scale(1.2) rotate(-7deg) translateY(-1px);
filter: drop-shadow(var(--glow-subtle-gold));
}
.nav-cart-count {
position: absolute;
top: 5px;
right: 5px;
background: var(--accent-moonbeam-gold);
color: var(--bg-celestial-white);
font-size: 0.75rem;
font-weight: 600;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 3px 10px rgba(212, 175, 55, 0.4);
border: 2px solid var(--bg-celestial-white);
font-family: var(--font-body);
}
.cart-aura-pulse {
position: absolute;
top: 50%;
left: 50%;
border-radius: 50%;
pointer-events: none;
transform: translate(-50%, -50%);
opacity: 0;
border: 2px solid;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 3rem;
padding: 4rem 0;
}
.product-card {
background-color: var(--bg-card);
border-radius: var(--radius-celestial);
box-shadow: var(--shadow-celestial);
transition: transform var(--transition-celestial),
box-shadow var(--transition-celestial);
display: flex;
flex-direction: column;
position: relative;
border: 1px solid var(--border-ethereal);
overflow: hidden;
}
.product-card:hover {
transform: translateY(-12px) scale(1.02);
box-shadow: 0 22px 50px rgba(107, 127, 157, 0.12);
}
.product-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: var(--radius-celestial);
box-shadow: inset 0 0 15px rgba(216, 191, 216, 0);
transition: box-shadow 0.5s ease;
pointer-events: none;
z-index: 1;
}
.product-card:hover::before {
box-shadow: inset 0 0 20px rgba(216, 191, 216, 0.4);
}
.product-card__image-container {
position: relative;
height: 280px;
overflow: hidden;
background: linear-gradient(160deg, #fdfeff, #f7f9fc);
border-radius: var(--radius-celestial) var(--radius-celestial) 0 0;
}
.product-card__image-container img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.9s cubic-bezier(0.16, 1, 0.3, 1), filter 0.6s ease;
}
.product-card:hover .product-card__image-container img {
transform: scale(1.1);
filter: saturate(1.1) brightness(1.02);
}
.product-card__info {
padding: 2rem;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.product-card__title {
font-family: var(--font-heading);
font-size: 1.6rem;
font-weight: 600;
margin-bottom: 0.7rem;
color: var(--text-deep-sky);
line-height: 1.3;
}
.product-card__price {
font-size: 1.3rem;
font-weight: 500;
color: var(--accent-moonbeam-gold);
margin-bottom: 1.2rem;
font-family: var(--font-body);
font-weight: 600;
}
.product-card__description {
font-size: 0.95rem;
color: var(--text-soft-sky);
flex-grow: 1;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.add-to-cart-btn {
background: var(--aura-gradient);
color: #fff;
border: none;
padding: 0.9rem 2.2rem;
border-radius: var(--radius-button-soft);
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: transform 0.35s ease, box-shadow 0.4s ease, filter 0.35s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.7rem;
margin-top: auto;
box-shadow: var(--shadow-interactive-light);
letter-spacing: 0.2px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.add-to-cart-btn:hover:not(:disabled) {
transform: translateY(-4px) scale(1.03);
box-shadow: 0 12px 28px rgba(212, 175, 55, 0.25);
filter: brightness(1.1) saturate(1.15);
}
.add-to-cart-btn.added {
background: var(--success-celestial);
pointer-events: none;
color: #2c5282;
box-shadow: 0 5px 15px rgba(166, 215, 174, 0.5);
}
.add-to-cart-btn.added .btn-text,
.add-to-cart-btn.added .btn-icon {
display: none;
}
.add-to-cart-btn.added::after {
content: "Carried Away 🕊️";
font-weight: 600;
}
.magnetic-aura {
position: fixed;
width: 28px;
height: 28px;
border-radius: 50%;
z-index: 1500;
pointer-events: none;
opacity: 0;
display: flex;
justify-content: center;
align-items: center;
background: radial-gradient(
circle,
rgba(255, 255, 255, 0.9) 5%,
var(--accent-aura-blue) 40%,
var(--accent-aura-lavender) 75%,
transparent 100%
);
box-shadow: var(--glow-aura);
animation: auraFloat 3.5s infinite ease-in-out alternate,
auraPulse 2.2s infinite ease-in-out;
filter: blur(1px);
}
@keyframes auraFloat {
0% {
transform: translateY(0px) translateX(0px) scale(var(--aura-scale, 1));
}
25% {
transform: translateY(-2px) translateX(1px) scale(var(--aura-scale, 1));
}
50% {
transform: translateY(1px) translateX(-1px) scale(var(--aura-scale, 1));
}
75% {
transform: translateY(-1px) translateX(2px) scale(var(--aura-scale, 1));
}
100% {
transform: translateY(0px) translateX(0px) scale(var(--aura-scale, 1));
}
}
@keyframes auraPulse {
0%,
100% {
filter: brightness(0.9) blur(1px) saturate(0.9);
opacity: 0.8;
}
50% {
filter: brightness(1.15) blur(0.5px) saturate(1.1);
opacity: 1;
}
}
/* Cart Panel - Ensure it does not affect main content flow */
.cart-overlay {
background-color: rgba(107, 127, 157, 0.3);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1900;
opacity: 0;
visibility: hidden;
}
.cart-panel {
max-width: 430px;
box-shadow: -12px 0 45px rgba(107, 127, 157, 0.18);
border-left: 1px solid var(--border-ethereal);
position: fixed;
top: 0;
right: -100%; /* Off-screen initially */
width: 100%;
height: 100%;
background-color: var(--bg-panel);
z-index: 2000;
display: flex;
flex-direction: column;
}
.cart-panel__header {
padding: 1.75rem 2rem;
border-bottom: 1px solid var(--border-ethereal);
}
.cart-panel__title {
font-family: var(--font-heading);
font-size: 1.8rem;
font-weight: 600;
}
.cart-panel__title i {
color: var(--accent-moonbeam-gold);
filter: drop-shadow(var(--glow-subtle-gold));
}
.cart-panel__close-btn:hover {
color: var(--accent-moonbeam-gold);
transform: rotate(270deg) scale(1.1);
}
.cart-panel__content-area {
/* New wrapper for items and empty message */
flex-grow: 1;
overflow-y: auto;
padding: 2rem;
position: relative;
}
.cart-panel__content-area::-webkit-scrollbar {
width: 6px;
}
.cart-panel__content-area::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.03);
border-radius: 10px;
}
.cart-panel__content-area::-webkit-scrollbar-thumb {
background: var(--accent-aura-lavender);
border-radius: 10px;
}
.cart-panel__content-area::-webkit-scrollbar-thumb:hover {
background: var(--accent-aura-blue);
}
#cartItemsContainer {
/* Actual list of items */
position: relative; /* For animations if needed */
}
.cart-panel__empty-cart {
/* Positioned absolutely within content-area or handled via display none/flex */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%; /* Take full height of parent if displayed */
color: var(--text-soft-sky);
text-align: center;
opacity: 0; /* Start hidden */
position: absolute;
top: 0;
left: 0;
width: 100%; /* Ensure it covers area if needed */
pointer-events: none; /* Don't block items if underneath */
}
.cart-panel__empty-cart.visible {
opacity: 1;
pointer-events: auto;
} /* Control visibility with JS */
.cart-panel__empty-cart i {
font-size: 6rem;
opacity: 0.6;
color: var(--text-whisper);
margin-bottom: 1.5rem;
}
.cart-panel__empty-cart p {
font-size: 1.15rem;
font-family: var(--font-heading);
font-weight: 500;
}
.cart-item {
padding: 1.5rem;
border-radius: var(--radius-celestial);
background-color: var(--bg-celestial-white);
margin-bottom: 1.5rem;
border: 1px solid var(--border-ethereal);
box-shadow: 0 6px 18px rgba(107, 127, 157, 0.06);
display: flex;
gap: 1.5rem;
position: relative;
}
.cart-item__img-container {
width: 85px;
height: 85px;
border-radius: var(--radius-celestial);
overflow: hidden;
flex-shrink: 0;
background-color: #f0f2f7;
}
.cart-item__img-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cart-item__info {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.cart-item__title {
font-family: var(--font-heading);
font-weight: 600;
font-size: 1.2rem;
margin-bottom: 0.3rem;
}
.cart-item__price {
color: var(--accent-moonbeam-gold);
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.5rem;
}
.cart-item__quantity-controls {
display: flex;
align-items: center;
gap: 0.75rem;
}
.quantity-btn {
width: 32px;
height: 32px;
background-color: var(--bg-card);
transition: all 0.4s var(--transition-celestial);
border: 1px solid var(--border-ethereal);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--text-soft-sky);
font-size: 0.9rem;
}
.quantity-btn:hover {
background-color: var(--accent-moonbeam-gold);
color: var(--bg-celestial-white);
border-color: var(--accent-moonbeam-gold);
transform: scale(1.12) rotate(10deg);
}
.cart-item__quantity {
font-weight: 600;
font-size: 1rem;
min-width: 20px;
text-align: center;
}
.cart-item__remove-btn {
position: absolute;
top: 1rem;
right: 1rem;
color: var(--text-whisper);
background: none;
border: none;
cursor: pointer;
font-size: 1.3rem;
padding: 0.2rem;
transition: color 0.3s ease, transform 0.3s ease;
}
.cart-item__remove-btn:hover {
color: var(--accent-flow-end);
transform: scale(1.2) rotate(15deg);
}
.cart-panel__footer {
background-color: var(--bg-celestial-white);
padding: 2rem;
border-top: 1px solid var(--border-ethereal);
}
.cart-panel__total {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.cart-panel__total span:first-child {
font-size: 1.15rem;
font-weight: 500;
color: var(--text-soft-sky);
}
.cart-panel__total-amount {
font-size: 1.6rem;
color: var(--accent-moonbeam-gold);
font-weight: 700;
}
.checkout-btn {
font-size: 1.1rem;
padding: 1.05rem;
box-shadow: var(--shadow-interactive-light);
border-radius: var(--radius-button-soft);
}
.checkout-btn:hover:not(:disabled) {
box-shadow: 0 14px 30px rgba(212, 175, 55, 0.3);
filter: brightness(1.08);
}
@media (max-width: 992px) {
.products-grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2.5rem;
}
}
@media (max-width: 768px) {
.container {
padding: 0 1.5rem;
}
.product-card__title {
font-size: 1.4rem;
}
.cart-panel {
max-width: 90vw;
}
}
@media (max-width: 480px) {
.logo {
font-size: 1.8rem;
}
.logo i {
font-size: 1.6rem;
}
.nav-cart-icon {
font-size: 1.7rem;
}
.product-card__image-container {
height: 250px;
}
.cart-panel__title {
font-size: 1.5rem;
}
.cart-item {
padding: 1.25rem;
}
.cart-panel__content-area,
.cart-panel__footer {
padding: 1.5rem;
}
}
document.addEventListener("DOMContentLoaded", () => {
AOS.init({
duration: 1100,
delay: 100,
once: true,
offset: 100,
easing: "cubic-bezier(0.19, 1, 0.22, 1)",
});
const productData = [
{
id: "cc01",
name: "Moonbeam Silk Robe",
price: 210.0,
image:
"https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8cm9iZXxlbnwwfHwwfHx8MA&auto=format&fit=crop&w=400&h=550&q=80",
},
{
id: "cc02",
name: "Star-Chart Telescope",
price: 349.5,
image:
"https://images.unsplash.com/photo-1600456548090-7d1b3f0bbea5?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8dGVsZXNjb3BlfGVufDB8fDB8fHww",
},
{
id: "cc03",
name: "Celestial Incense Set",
price: 72.0,
image:
"https://images.unsplash.com/photo-1612704057720-e8f66bade6ca?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8aW5jZW5zZSUyMGJ1cm5lcnxlbnwwfHwwfHx8MA%3D%3D",
},
{
id: "cc04",
name: "Aurora Borealis Print",
price: 95.0,
image:
"https://plus.unsplash.com/premium_photo-1673254850380-ff70514979fe?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8YXVyb3JhJTIwYm9yZWFsaXN8ZW58MHx8MHx8fDA%3D",
},
{
id: "cc05",
name: "Crystal Geode Bookends",
price: 130.99,
image:
"https://rockparadise.com/cdn/shop/products/bookends-geode-book-end-natural-agate-bookend-pair-1-to-3-lb-geode-bookend-home-decor-crystal-and-stones-bke-1_720x.jpg?v=1571609053",
},
{
id: "cc06",
name: "Lavender Dream Pillow Mist",
price: 38.75,
image:
"https://weavvehome.com/cdn/shop/products/CHR08172.jpg?v=1691153613&width=2000",
},
];
// DOM Cache
const productsGrid = document.getElementById("productsGrid");
const navCartIconWrapper = document.getElementById("navCartIconWrapper");
const navCartCount = document.getElementById("navCartCount");
const cartOverlay = document.getElementById("cartOverlay");
const cartPanel = document.getElementById("cartPanel");
const closeCartBtn = document.getElementById("closeCartBtn");
const cartItemsContainer = document.getElementById("cartItemsContainer"); // For cart items list
const emptyCartMessage = document.getElementById("emptyCartMessage"); // The empty message div
const cartTotalAmount = document.getElementById("cartTotalAmount");
const checkoutBtn = document.getElementById("checkoutBtn");
let cart = [];
let activeMagneticAura = null;
let magneticPullRAF = null;
function renderProducts() {
productsGrid.innerHTML = "";
productData.forEach((product, index) => {
const productCard = document.createElement("div");
productCard.className = "product-card";
const aosType = index % 2 === 0 ? "zoom-in-right" : "zoom-in-left";
productCard.setAttribute("data-aos", aosType);
productCard.setAttribute("data-aos-delay", (index % 3) * 120 + 150);
productCard.setAttribute("data-aos-duration", "900");
productCard.setAttribute("data-aos-once", "true");
productCard.innerHTML = `
<div class="product-card__image-container">
<img src="${product.image}" alt="${
product.name
}" id="product-img-${product.id}">
</div>
<div class="product-card__info">
<h3 class="product-card__title">${product.name}</h3>
<p class="product-card__price">$${product.price.toFixed(
2
)}</p>
<p class="product-card__description">${
product.description
}</p>
<button class="add-to-cart-btn" data-id="${product.id}">
<span class="btn-text">Add to Bag</span>
<i class="fas fa-meteor btn-icon"></i>
</button>
</div>
`;
productsGrid.appendChild(productCard);
});
}
function createMagneticAura(buttonElement) {
if (activeMagneticAura) {
gsap.killTweensOf(activeMagneticAura);
activeMagneticAura.remove();
cancelAnimationFrame(magneticPullRAF);
magneticPullRAF = null;
}
const buttonRect = buttonElement.getBoundingClientRect();
const aura = document.createElement("div");
aura.className = "magnetic-aura";
document.body.appendChild(aura);
activeMagneticAura = aura;
gsap.set(aura, {
left: buttonRect.left + buttonRect.width / 2 - aura.offsetWidth / 2,
top: buttonRect.top + buttonRect.height / 2 - aura.offsetHeight / 2,
opacity: 0,
scale: 0.2,
"--aura-scale": 0.2,
});
gsap.to(aura, {
opacity: 0.9,
scale: 1,
"--aura-scale": 1,
duration: 0.6,
ease: "elastic.out(0.8, 0.5)",
});
aura.physics = {
x: parseFloat(gsap.getProperty(aura, "left")),
y: parseFloat(gsap.getProperty(aura, "top")),
vx: (Math.random() - 0.5) * 3,
vy: (Math.random() - 0.5) * 3 - 1.5,
};
}
function updateMagneticPull() {
if (!activeMagneticAura) {
magneticPullRAF = null;
return;
}
const cartRect = navCartIconWrapper.getBoundingClientRect();
const cartCenterX = cartRect.left + cartRect.width / 2;
const cartCenterY = cartRect.top + cartRect.height / 2;
const auraPhysics = activeMagneticAura.physics;
const auraCenterX = auraPhysics.x + activeMagneticAura.offsetWidth / 2;
const auraCenterY = auraPhysics.y + activeMagneticAura.offsetHeight / 2;
const dx = cartCenterX - auraCenterX;
const dy = cartCenterY - auraCenterY;
let distance = Math.sqrt(dx * dx + dy * dy);
distance = Math.max(distance, 1);
const magneticFieldRadius = 160;
if (isMouseNearCart && distance < magneticFieldRadius * 2) {
let forceMagnitude = 50 / (distance + 30);
forceMagnitude = Math.min(forceMagnitude, 1.2);
if (distance < magneticFieldRadius * 0.4) {
forceMagnitude *= 3.5;
} else if (distance < magneticFieldRadius * 0.8) {
forceMagnitude *= 2.0;
}
const ax = (dx / distance) * forceMagnitude;
const ay = (dy / distance) * forceMagnitude;
auraPhysics.vx += ax;
auraPhysics.vy += ay;
auraPhysics.vx *= 0.925;
auraPhysics.vy *= 0.925;
auraPhysics.x += auraPhysics.vx;
auraPhysics.y += auraPhysics.vy;
gsap.set(activeMagneticAura, {
left: auraPhysics.x,
top: auraPhysics.y,
});
if (distance < 8) {
finalizeMagneticAura();
return;
}
} else {
auraPhysics.vx *= 0.975;
auraPhysics.vy *= 0.975;
auraPhysics.x += auraPhysics.vx;
auraPhysics.y += auraPhysics.vy;
if (Math.abs(auraPhysics.vx) < 0.1 && Math.abs(auraPhysics.vy) < 0.1) {
auraPhysics.vx += (Math.random() - 0.5) * 0.1;
auraPhysics.vy += (Math.random() - 0.5) * 0.1;
}
gsap.set(activeMagneticAura, {
left: auraPhysics.x,
top: auraPhysics.y,
});
}
magneticPullRAF = requestAnimationFrame(updateMagneticPull);
}
function finalizeMagneticAura() {
if (!activeMagneticAura) return;
const auraToFinalize = activeMagneticAura;
activeMagneticAura = null;
cancelAnimationFrame(magneticPullRAF);
magneticPullRAF = null;
const cartRect = navCartIconWrapper.getBoundingClientRect();
gsap.to(auraToFinalize, {
left: cartRect.left + cartRect.width / 2 - auraToFinalize.offsetWidth / 2,
top: cartRect.top + cartRect.height / 2 - auraToFinalize.offsetHeight / 2,
scale: 0,
opacity: 0,
duration: 0.4,
ease: "power3.in",
onComplete: () => {
auraToFinalize.remove();
const cartIcon = navCartIconWrapper.querySelector(".nav-cart-icon");
gsap
.timeline()
.to(cartIcon, {
scale: 1.4,
duration: 0.15,
ease: "power2.out",
})
.to(cartIcon, {
scale: 1,
duration: 0.3,
ease: "elastic.out(1, 0.5)",
});
createRippleEffect(true);
},
});
}
let isMouseNearCart = false;
let rippleInterval;
function createRippleEffect(isFinal = false) {
const ripple = document.createElement("div");
ripple.className = "cart-aura-pulse";
const rippleColor = [
getComputedStyle(document.documentElement)
.getPropertyValue("--accent-aura-blue")
.trim(),
getComputedStyle(document.documentElement)
.getPropertyValue("--accent-aura-lavender")
.trim(),
getComputedStyle(document.documentElement)
.getPropertyValue("--accent-moonbeam-gold")
.trim(),
][Math.floor(Math.random() * 3)];
ripple.style.borderColor = rippleColor;
ripple.style.boxShadow = `0 0 10px ${rippleColor}, 0 0 15px ${rippleColor}`;
navCartIconWrapper.appendChild(ripple);
const startSize = isFinal ? "70px" : "60px";
const endSize = isFinal ? "150px" : "130px";
const duration = isFinal ? 1.8 : 1.5;
gsap.set(ripple, {
width: startSize,
height: startSize,
opacity: isFinal ? 0.8 : 0.6,
});
gsap.to(ripple, {
width: endSize,
height: endSize,
opacity: 0,
duration: duration,
ease: "circ.out",
onComplete: () => ripple.remove(),
});
}
navCartIconWrapper.addEventListener("mouseenter", () => {
isMouseNearCart = true;
if (!magneticPullRAF && activeMagneticAura) {
magneticPullRAF = requestAnimationFrame(updateMagneticPull);
}
if (!rippleInterval) {
createRippleEffect();
rippleInterval = setInterval(() => createRippleEffect(false), 700);
}
});
navCartIconWrapper.addEventListener("mouseleave", () => {
isMouseNearCart = false;
clearInterval(rippleInterval);
rippleInterval = null;
});
function handleAddToCart(event) {
const button = event.target.closest(".add-to-cart-btn");
if (!button || button.classList.contains("added")) return;
createMagneticAura(button);
if (!magneticPullRAF) {
magneticPullRAF = requestAnimationFrame(updateMagneticPull);
}
button.classList.add("added");
setTimeout(() => {
button.classList.remove("added");
}, 2000);
const productId = button.dataset.id;
const product = productData.find((p) => p.id === productId);
if (!product) return;
const existingItem = cart.find((item) => item.id === productId);
if (existingItem) {
existingItem.quantity++;
} else {
cart.push({ ...product, quantity: 1 });
}
updateCart();
}
function updateCart() {
renderCartItems();
updateCartCount();
updateCartTotal();
updateCheckoutButtonState();
}
function renderCartItems() {
// Manage visibility of empty message first
if (cart.length === 0) {
if (!emptyCartMessage.classList.contains("visible")) {
gsap.to(emptyCartMessage, {
opacity: 1,
duration: 0.4,
ease: "power2.out",
onStart: () => emptyCartMessage.classList.add("visible"),
});
}
} else {
if (emptyCartMessage.classList.contains("visible")) {
gsap.to(emptyCartMessage, {
opacity: 0,
duration: 0.3,
ease: "power2.in",
onComplete: () => emptyCartMessage.classList.remove("visible"),
});
}
}
// Animate removal of items no longer in cart
const currentDOMItemIds = Array.from(
cartItemsContainer.querySelectorAll(".cart-item")
).map((el) => el.dataset.itemId);
const cartItemIds = cart.map((item) => item.id);
currentDOMItemIds.forEach((domId) => {
if (!cartItemIds.includes(domId)) {
const domItem = cartItemsContainer.querySelector(
`.cart-item[data-item-id="${domId}"]`
);
if (domItem) {
gsap.to(domItem, {
opacity: 0,
height: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
duration: 0.4,
ease: "power2.in",
onComplete: () => domItem.remove(),
});
}
}
});
// Add or update items
// Use a slight delay to ensure removal animations can complete before adding new items if lists were cleared completely
gsap.delayedCall(
cart.length > 0 && currentDOMItemIds.length === 0 ? 0.1 : 0,
() => {
cart.forEach((item, index) => {
let cartItemDiv = cartItemsContainer.querySelector(
`.cart-item[data-item-id="${item.id}"]`
);
if (!cartItemDiv) {
// New item
cartItemDiv = document.createElement("div");
cartItemDiv.className = "cart-item";
cartItemDiv.dataset.itemId = item.id; // Important for tracking
gsap.set(cartItemDiv, { opacity: 0, y: 30 }); // Start off-screen for entry
cartItemDiv.innerHTML = `
<div class="cart-item__img-container"><img src="${
item.image
}" alt="${item.name}"></div>
<div class="cart-item__info">
<div><h4 class="cart-item__title">${
item.name
}</h4><p class="cart-item__price">$${(
item.price * 1
).toFixed(2)}</p></div>
<div class="cart-item__quantity-controls">
<button class="quantity-btn decrease-qty" data-id="${
item.id
}"><i class="fas fa-minus"></i></button>
<span class="cart-item__quantity">${
item.quantity
}</span>
<button class="quantity-btn increase-qty" data-id="${
item.id
}"><i class="fas fa-plus"></i></button>
</div>
</div>
<button class="cart-item__remove-btn" data-id="${
item.id
}"><i class="fas fa-times"></i></button>
`;
cartItemsContainer.appendChild(cartItemDiv);
gsap.to(cartItemDiv, {
opacity: 1,
y: 0,
duration: 0.5,
delay: index * 0.08,
ease: "circ.out",
});
} else {
// Existing item, update quantity and price
const quantitySpan = cartItemDiv.querySelector(
".cart-item__quantity"
);
const priceSpan = cartItemDiv.querySelector(".cart-item__price");
if (parseInt(quantitySpan.textContent) !== item.quantity) {
// Animate quantity change
gsap
.timeline()
.to(quantitySpan, {
scale: 1.4,
opacity: 0.6,
duration: 0.15,
ease: "power1.out",
})
.set(quantitySpan, { textContent: item.quantity })
.to(quantitySpan, {
scale: 1,
opacity: 1,
duration: 0.2,
ease: "elastic.out(1, 0.6)",
});
}
priceSpan.textContent = `$${(item.price * item.quantity).toFixed(
2
)}`;
}
});
}
);
}
function updateCartCount() {
const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
const prevCount = parseInt(navCartCount.dataset.prevCount || "0");
navCartCount.textContent = totalItems;
if (totalItems !== prevCount) {
gsap.fromTo(
navCartCount,
{ scale: 1.7, opacity: 0, y: -5 },
{
scale: 1,
opacity: 1,
y: 0,
duration: 0.5,
ease: "elastic.out(1.1, 0.5)",
}
);
}
navCartCount.dataset.prevCount = totalItems;
}
function updateCartTotal() {
const total = cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
cartTotalAmount.textContent = `$${total.toFixed(2)}`;
}
function updateCheckoutButtonState() {
checkoutBtn.disabled = cart.length === 0;
}
function handleCartActions(event) {
const target = event.target.closest("button");
if (!target) return;
const productId = target.dataset.id;
const itemIndex = cart.findIndex((i) => i.id === productId);
if (itemIndex === -1 && !target.classList.contains("cart-item__remove-btn"))
return;
if (target.classList.contains("increase-qty")) {
cart[itemIndex].quantity++;
} else if (target.classList.contains("decrease-qty")) {
cart[itemIndex].quantity--;
if (cart[itemIndex].quantity <= 0) {
cart.splice(itemIndex, 1);
}
} else if (target.classList.contains("cart-item__remove-btn")) {
if (itemIndex !== -1) cart.splice(itemIndex, 1);
}
updateCart();
}
function openCartPanel() {
if (activeMagneticAura) finalizeMagneticAura();
cartOverlay.style.visibility = "visible";
gsap.to(cartOverlay, {
opacity: 1,
duration: 0.55,
ease: "power3.out",
});
gsap.to(cartPanel, {
right: 0,
duration: 0.75,
ease: "expo.out",
onStart: updateCart,
});
document.body.style.overflow = "hidden";
}
function closeCartPanel() {
gsap.to(cartPanel, {
right: "-100%",
duration: 0.65,
ease: "expo.in",
});
gsap.to(cartOverlay, {
opacity: 0,
duration: 0.45,
ease: "power3.in",
onComplete: () => {
cartOverlay.style.visibility = "hidden";
document.body.style.overflow = "";
},
});
}
productsGrid.addEventListener("click", handleAddToCart);
navCartIconWrapper.addEventListener("click", openCartPanel);
closeCartBtn.addEventListener("click", closeCartPanel);
cartOverlay.addEventListener("click", closeCartPanel);
cartItemsContainer.addEventListener("click", handleCartActions);
checkoutBtn.addEventListener("click", () => {
if (cart.length > 0) {
alert(
`Journeying to checkout with $${cartTotalAmount.textContent}.\n(Celestial Cart Demo)`
);
cart = [];
updateCart();
closeCartPanel();
}
});
renderProducts();
updateCart(); // Initial setup
});
VIDEO
Download Source Code
Get the complete source code for this tutorial to use in your projects.
Start 1-Minute Timer for Download
Please wait...
Timer paused. Please stay on this page to continue.
1
0
Comments (1)