JSbanner-cartly.jsโ GSAP animation timeline
(() => {
const root = document.getElementById('cartly');
if (!root) return;
// -------------------------------------------------------------------
// SLIDES
// -------------------------------------------------------------------
const slides = [
{
titleHtml: 'Get an Easier and More Enjoyable Online <span class="hl-coral">Shopping Experience</span>',
desc: 'Get inspiration and creative ideas to beautify your design with our products, get attractive prizes by purchasing certain products.',
cta: 'Join Now',
badgePct: '50',
badgeLabel: 'OFF',
portrait: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=crop&w=700&q=80',
stats: { customer: '467K', traffic: '35.0M', sale: '150K' },
},
{
titleHtml: 'Fast and Reliable Doorstep <span class="hl-coral">Delivery Service</span> Nationwide',
desc: 'Track every order in real time with live updates, photo proof of delivery, and instant refunds if anything goes sideways along the route.',
cta: 'Track Order',
badgePct: 'FREE',
badgeLabel: 'SHIP',
portrait: 'https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?auto=format&fit=crop&w=700&q=80',
stats: { customer: '982K', traffic: '52.4M', sale: '230K' },
},
{
titleHtml: 'Handpicked Products for <span class="hl-coral">Premium Quality</span> Every Time',
desc: 'Every item is tested by our team of product specialists before it reaches your cart, so what you see is exactly what shows up on your doorstep.',
cta: 'Explore Collection',
badgePct: 'NEW',
badgeLabel: 'ARRIVALS',
portrait: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=700&q=80',
stats: { customer: '1.2M', traffic: '68.7M', sale: '340K' },
},
{
titleHtml: 'Join Our Growing Global <span class="hl-coral">Shopping Community</span> Today',
desc: 'Members-only drops, early access to sales, and a loyalty program that pays you back every single time you complete a purchase. No catches.',
cta: 'Sign Up Free',
badgePct: 'VIP',
badgeLabel: 'PERKS',
portrait: 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&w=700&q=80',
stats: { customer: '2.4M', traffic: '98.1M', sale: '512K' },
},
];
let currentSlide = 0;
// -------------------------------------------------------------------
// BUILD LOADER SPARKLES
// -------------------------------------------------------------------
const sparklesHost = document.getElementById('loader-sparkles');
const SPARK_COUNT = 8;
for (let i = 0; i < SPARK_COUNT; i++) {
const s = document.createElement('span');
s.textContent = '\u2728';
const angle = (i / SPARK_COUNT) * Math.PI * 2;
s.style.left = (110 + Math.cos(angle) * 90) + 'px';
s.style.top = (110 + Math.sin(angle) * 90) + 'px';
s.style.transform = 'translate(-50%, -50%)';
sparklesHost.appendChild(s);
}
const sparkles = sparklesHost.querySelectorAll('span');
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const loaderDashed = document.getElementById('loader-dashed');
const loaderBadge = document.getElementById('loader-badge');
const loaderPct = document.getElementById('loader-pct');
const magnetics = root.querySelectorAll('[data-magnetic]');
const titleEl = document.getElementById('title');
const descEl = document.getElementById('desc');
const ctaLabel = document.getElementById('cta-label');
const ctaPill = root.querySelector('[data-cta]');
const portrait = document.getElementById('portrait');
const portraitImg = document.getElementById('portrait-img');
const discountBadge = document.getElementById('discount-badge');
const badgePct = document.getElementById('badge-pct');
const badgeLabel = document.getElementById('badge-label');
const decoRing = document.getElementById('deco-ring');
const decoCurve = document.getElementById('deco-curve');
const stage = document.getElementById('stage');
const statsCard = document.getElementById('stats-card');
const statEls = root.querySelectorAll('[data-stat]');
const dots = root.querySelectorAll('.dots span');
const logoMark = document.getElementById('logo-mark');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -15, opacity: 0 });
gsap.set(descEl, { y: 20, opacity: 0 });
gsap.set(ctaPill, { scale: 0.9, opacity: 0 });
gsap.set(portrait, { scale: 0.85, opacity: 0 });
gsap.set(discountBadge, { scale: 0, opacity: 0, rotation: -90 });
gsap.set(decoRing, { opacity: 0, rotation: -45, transformOrigin: '250px 250px' });
gsap.set(decoCurve, { opacity: 0 });
gsap.set(statsCard, { y: 30, opacity: 0 });
gsap.set(dots, { opacity: 0 });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Dashed ring rotates
gsap.to(loaderDashed, {
rotation: 360,
duration: 6,
repeat: -1,
ease: 'none',
transformOrigin: 'center',
});
// Badge pulses
gsap.to(loaderBadge, {
scale: 1.06,
duration: 1.2,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Sparkles twinkle
sparkles.forEach((s, i) => {
gsap.fromTo(s,
{ scale: 0, opacity: 0 },
{
scale: 1.2, opacity: 1,
duration: 0.6,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.15,
}
);
});
// Percentage counter in badge
const bp = { v: 0 };
loaderTl.to(bp, {
v: 50,
duration: 2.2,
ease: 'power2.out',
onUpdate: () => { loaderPct.textContent = Math.floor(bp.v); },
}, 0);
// Progress counter + labels
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2.2,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = Math.floor(p.v) + '%';
if (p.v > 30 && loaderLabel.textContent === 'UNLOCKING DEALS') loaderLabel.textContent = 'STOCKING SHELVES';
if (p.v > 65 && loaderLabel.textContent === 'STOCKING SHELVES') loaderLabel.textContent = 'ALMOST OPEN';
if (p.v > 95 && loaderLabel.textContent === 'ALMOST OPEN') loaderLabel.textContent = 'READY TO SHOP';
},
}, 0);
// Exit
loaderTl.to([loaderCounter, loaderLabel], {
y: -10, opacity: 0, duration: 0.3, stagger: 0.05,
}, '+=0.3');
loaderTl.to('.loader__stage', {
scale: 1.3, opacity: 0, duration: 0.7, ease: 'power3.in',
}, '-=0.3');
loaderTl.to(loader, {
opacity: 0, duration: 0.4, ease: 'power2.inOut',
}, '-=0.3');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// RENDER SLIDE
// -------------------------------------------------------------------
let titleChars = [];
function wrapWordChars(word, parent) {
const wordSpan = document.createElement('span');
wordSpan.className = 'hero__word';
[...word].forEach((ch) => {
const charSpan = document.createElement('span');
charSpan.className = 'hero__char';
charSpan.textContent = ch;
wordSpan.appendChild(charSpan);
});
parent.appendChild(wordSpan);
}
function splitTitle(html) {
// Render html first so coral span is available
titleEl.innerHTML = html;
// Clone children, split text nodes into words, wrap each word
const children = [...titleEl.childNodes];
titleEl.innerHTML = '';
children.forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
const words = node.textContent.split(/\s+/).filter(Boolean);
words.forEach((w) => wrapWordChars(w, titleEl));
} else if (node.nodeType === Node.ELEMENT_NODE) {
// Preserve element (e.g., .hl-coral) and wrap its words inside it
const wrapper = document.createElement(node.tagName);
wrapper.className = node.className;
const words = node.textContent.split(/\s+/).filter(Boolean);
words.forEach((w) => wrapWordChars(w, wrapper));
titleEl.appendChild(wrapper);
}
});
return titleEl.querySelectorAll('.hero__char');
}
function parseStatNum(str) {
// "467K" -> 467, "35.0M" -> 35, "1.2M" -> 1.2
const m = str.match(/([\d.]+)/);
return m ? parseFloat(m[1]) : 0;
}
function animateStat(el, targetStr) {
const target = parseStatNum(targetStr);
const suffix = targetStr.replace(/[\d.]+/, '');
const decimal = targetStr.includes('.');
const obj = { v: 0 };
gsap.to(obj, {
v: target,
duration: 1.2,
ease: 'power2.out',
onUpdate: () => {
const val = decimal ? obj.v.toFixed(1) : Math.floor(obj.v);
el.textContent = val + suffix;
},
});
}
function renderSlide(idx, direction = 1) {
const slide = slides[idx];
const outTl = gsap.timeline();
outTl.to(titleChars, {
yPercent: -110, opacity: 0,
duration: 0.35,
stagger: 0.012,
ease: 'power2.in',
}, 0);
outTl.to([descEl, ctaLabel], {
y: -15, opacity: 0, duration: 0.3, ease: 'power2.in',
}, 0);
outTl.to(portrait, {
scale: 0.9, opacity: 0.3, duration: 0.4, ease: 'power2.in',
}, 0);
outTl.to(discountBadge, {
scale: 0.6, opacity: 0, rotation: 180 * direction,
duration: 0.4, ease: 'power2.in',
}, 0);
outTl.to(statsCard, {
y: 15, opacity: 0, duration: 0.3, ease: 'power2.in',
}, 0);
outTl.call(() => {
// Rebuild title
titleChars = splitTitle(slide.titleHtml);
descEl.textContent = slide.desc;
ctaLabel.textContent = slide.cta;
portraitImg.src = slide.portrait;
badgePct.textContent = slide.badgePct;
badgeLabel.textContent = slide.badgeLabel;
// Update dots
dots.forEach((d, i) => d.classList.toggle('is-active', i === idx));
// Animate in
gsap.set(titleChars, { yPercent: 110, opacity: 0 });
gsap.set([descEl, ctaLabel], { y: 15, opacity: 0 });
gsap.to(titleChars, {
yPercent: 0, opacity: 1,
duration: 0.7,
stagger: 0.012,
ease: 'power3.out',
});
gsap.to(descEl, {
y: 0, opacity: 1,
duration: 0.6,
delay: 0.25,
ease: 'power3.out',
});
gsap.to(ctaLabel, {
y: 0, opacity: 1,
duration: 0.5,
delay: 0.4,
ease: 'power3.out',
});
gsap.to(portrait, {
scale: 1, opacity: 1,
duration: 0.9,
ease: 'power3.out',
});
gsap.to(discountBadge, {
scale: 1, opacity: 1, rotation: 0,
duration: 0.7,
delay: 0.2,
ease: 'back.out(1.8)',
});
gsap.to(statsCard, {
y: 0, opacity: 1,
duration: 0.6,
delay: 0.35,
ease: 'power3.out',
onStart: () => {
animateStat(statEls[0], slide.stats.customer);
animateStat(statEls[1], slide.stats.traffic);
animateStat(statEls[2], slide.stats.sale);
},
});
attachTitleHover();
});
}
function attachTitleHover() {
titleChars.forEach((c) => {
c.addEventListener('mouseenter', () => gsap.to(c, { color: '#6c5ce7', duration: 0.2 }));
c.addEventListener('mouseleave', () => {
const isCoral = c.parentElement && c.parentElement.classList.contains('hl-coral');
gsap.to(c, { color: isCoral ? '#f47a76' : '#1a1b4e', duration: 0.4 });
});
});
}
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
// Initial content
const slide = slides[0];
titleChars = splitTitle(slide.titleHtml);
descEl.textContent = slide.desc;
ctaLabel.textContent = slide.cta;
badgePct.textContent = slide.badgePct;
badgeLabel.textContent = slide.badgeLabel;
gsap.set(titleChars, { yPercent: 110, opacity: 0 });
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Nav
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.04,
}, 0);
// Decorative ring + curve
tl.to(decoRing, {
opacity: 1, rotation: 0,
duration: 1.4,
ease: 'power4.out',
}, 0.3);
tl.to(decoCurve, { opacity: 1, duration: 0.8 }, 0.6);
// Portrait
tl.to(portrait, {
scale: 1, opacity: 1,
duration: 1.2,
ease: 'power4.out',
}, 0.5);
// Title
tl.to(titleChars, {
yPercent: 0, opacity: 1,
duration: 1,
stagger: 0.018,
ease: 'expo.out',
}, 0.7);
// Desc + CTA
tl.to(descEl, { y: 0, opacity: 1, duration: 0.7 }, 1.1);
tl.to(ctaPill, {
scale: 1, opacity: 1,
duration: 0.7,
ease: 'back.out(1.6)',
}, 1.3);
// Discount badge
tl.to(discountBadge, {
scale: 1, opacity: 1, rotation: 0,
duration: 0.9,
ease: 'back.out(1.8)',
}, 1.1);
// Stats
tl.to(statsCard, {
y: 0, opacity: 1,
duration: 0.8,
onStart: () => {
animateStat(statEls[0], slide.stats.customer);
animateStat(statEls[1], slide.stats.traffic);
animateStat(statEls[2], slide.stats.sale);
},
}, 1.5);
// Dots
tl.to(dots, { opacity: 1, duration: 0.3, stagger: 0.05 }, 1.7);
tl.call(() => {
attachTitleHover();
startContinuous();
enableInteractions();
}, null, 1.9);
}
// -------------------------------------------------------------------
// CONTINUOUS
// -------------------------------------------------------------------
function startContinuous() {
// Yellow dashed ring rotates continuously
gsap.to(decoRing, {
rotation: 360,
duration: 40,
repeat: -1,
ease: 'none',
transformOrigin: '250px 250px',
});
// Discount badge gentle bob
gsap.to(discountBadge, {
y: -8,
duration: 1.8,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic ----
magnetics.forEach((el) => {
const strength = el.classList.contains('cta-pill') ? 0.3
: el.classList.contains('discount-badge') ? 0.4
: el.classList.contains('side-arrow') ? 0.4
: el.classList.contains('topnav__logo') ? 0.2
: 0.22;
el.addEventListener('mousemove', (e) => {
const r = el.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
gsap.to(el, {
x: (e.clientX - cx) * strength,
y: (e.clientY - cy) * strength,
duration: 0.4, ease: 'power3.out',
});
});
el.addEventListener('mouseleave', () => {
gsap.to(el, { x: 0, y: 0, duration: 0.6, ease: 'elastic.out(1, 0.4)' });
});
});
// ---- Logo cart wheels spin ----
const logoEl = root.querySelector('.topnav__logo');
logoEl.addEventListener('mouseenter', () => {
gsap.to(logoMark.querySelectorAll('circle'), {
rotation: 720,
duration: 0.8,
ease: 'power3.inOut',
transformOrigin: 'center',
stagger: 0.05,
});
});
// ---- Discount badge rotates on hover ----
discountBadge.addEventListener('mouseenter', () => {
gsap.to(discountBadge, {
rotation: 15,
scale: 1.08,
duration: 0.4,
ease: 'back.out(2)',
});
});
discountBadge.addEventListener('mouseleave', () => {
gsap.to(discountBadge, {
rotation: 0, scale: 1,
duration: 0.5,
ease: 'elastic.out(1, 0.4)',
});
});
// ---- CTA click: confetti burst ----
ctaPill.addEventListener('click', (e) => {
e.preventDefault();
const rect = ctaPill.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const colors = ['#6c5ce7', '#f47a76', '#ffcd40', '#1a1b4e'];
for (let i = 0; i < 12; i++) {
const s = document.createElement('span');
s.style.cssText = `position:fixed;left:${cx}px;top:${cy}px;width:8px;height:8px;background:${colors[i % colors.length]};pointer-events:none;z-index:200;transform:translate(-50%,-50%);border-radius:2px;`;
document.body.appendChild(s);
const a = (i / 12) * Math.PI * 2;
const dist = 80 + Math.random() * 40;
gsap.fromTo(s,
{ x: 0, y: 0, opacity: 1, rotation: 0 },
{
x: Math.cos(a) * dist,
y: Math.sin(a) * dist,
opacity: 0,
scale: 0.3,
rotation: 360,
duration: 0.9,
ease: 'power2.out',
onComplete: () => s.remove(),
}
);
}
});
// ---- Stats card hover: lift ----
statsCard.addEventListener('mouseenter', () => {
gsap.to(statsCard, { y: -6, scale: 1.02, duration: 0.4, ease: 'back.out(2)' });
});
statsCard.addEventListener('mouseleave', () => {
gsap.to(statsCard, { y: 0, scale: 1, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
});
// ---- Slider arrows ----
const goSlide = (dir) => {
const next = (currentSlide + dir + slides.length) % slides.length;
currentSlide = next;
renderSlide(next, dir);
};
prevBtn.addEventListener('click', () => goSlide(-1));
nextBtn.addEventListener('click', () => goSlide(1));
// ---- Dots ----
dots.forEach((dot, i) => {
dot.addEventListener('click', () => {
if (i === currentSlide) return;
const direction = i > currentSlide ? 1 : -1;
currentSlide = i;
renderSlide(i, direction);
});
});
// ---- Keyboard ----
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') goSlide(-1);
if (e.key === 'ArrowRight') goSlide(1);
});
// ---- Auto-advance ----
let autoTimer = setInterval(() => goSlide(1), 8000);
root.addEventListener('mouseenter', () => clearInterval(autoTimer));
root.addEventListener('mouseleave', () => {
clearInterval(autoTimer);
autoTimer = setInterval(() => goSlide(1), 8000);
});
// ---- Portrait 3D tilt ----
let mx = 0, my = 0, cmx = 0, cmy = 0;
stage.addEventListener('mousemove', (e) => {
const r = stage.getBoundingClientRect();
mx = ((e.clientX - r.left) / r.width - 0.5) * 2;
my = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
stage.addEventListener('mouseleave', () => { mx = 0; my = 0; });
gsap.ticker.add(() => {
cmx += (mx - cmx) * 0.06;
cmy += (my - cmy) * 0.06;
gsap.set(portrait, {
rotationY: cmx * 8,
rotationX: -cmy * 5,
transformPerspective: 1000,
transformOrigin: 'center',
});
gsap.set(decoCurve, { x: -cmx * 12, y: -cmy * 8 });
});
}
})();