Add to cart animation

Celestial Cart: Ethereal E-commerce UI with Magnetic Aura Pull & GSAP Animations

May 25, 2025
50 views
CSS GSAP HTML JavaScript


Celestial Cart: Ethereal E-commerce UI with Magnetic Aura Pull & GSAP Animations


"Celestial Cart - Aura Pull" is an exceptionally refined e-commerce interface featuring a unique "magnetic pull" interaction. When an item is added, a soft, glowing aura emanates and is gently drawn towards the main cart icon if the user's mouse hovers nearby, accelerating as it approaches. The design embodies an ethereal, light aesthetic with luminous whites, soft sky blues, gentle lavenders, and pearlescent accents, complemented by delicate typography (Cormorant Garamond & Montserrat). GSAP orchestrates the fluid physics-based aura movement and all UI animations, while AOS provides graceful page-load effects. The slide-in cart panel is seamlessly integrated, offering a polished experience for managing selected items.

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.
HTML (html)
<!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>
CSS (css)
: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; } }
JAVASCRIPT (javascript)
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 });

Download Source Code

Get the complete source code for this tutorial to use in your projects.

Comments (1)

T
test
9 months ago
hello
(0)
Reply
?