JSbanner-nest.js— GSAP animation timeline
(() => {
const root = document.getElementById('nest');
if (!root) return;
// -------------------------------------------------------------------
// SLIDE DATA — 4 unique slides
// -------------------------------------------------------------------
const slides = [
{
titleHtml: 'Get <span class="hl-yellow">$350k</span> extra<br>to get a house of<br>your <span class="hl-box">Imagination</span> now',
desc: 'Tailored home loans and refinancing options built for first-time buyers, ambitious upgraders, and seasoned owners looking for their next address.',
cta: 'Start Owning',
counterTarget: 350000,
counterPrefix: '$',
counterSuffix: 'k',
pic: 'https://images.unsplash.com/photo-1576019350023-8ea21aa65a0a?auto=format&fit=crop&w=400&q=80',
pic1: 'https://images.unsplash.com/photo-1611162616305-c69b3fa7fbe0?auto=format&fit=crop&w=500&q=80',
pic2: 'https://images.unsplash.com/photo-1513151233558-d860c5398176?auto=format&fit=crop&w=500&q=80',
pic3: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&w=800&q=80',
},
{
titleHtml: 'Earn <span class="hl-yellow">12%</span> returns<br>on every smart<br><span class="hl-box">Investment</span> you make',
desc: 'A curated portfolio of vetted properties, fractional shares, and REITs designed to outperform inflation while you focus on the life you actually want.',
cta: 'Start Investing',
counterTarget: 12,
counterPrefix: '',
counterSuffix: '%',
pic: 'https://images.unsplash.com/photo-1579621970588-a35d0e7ab9b6?auto=format&fit=crop&w=400&q=80',
pic1: 'https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?auto=format&fit=crop&w=500&q=80',
pic2: 'https://images.unsplash.com/photo-1559526324-4b87b5e36e44?auto=format&fit=crop&w=500&q=80',
pic3: 'https://images.unsplash.com/photo-1600880292089-90a7e086ee0c?auto=format&fit=crop&w=800&q=80',
},
{
titleHtml: 'Save <span class="hl-yellow">$1,200</span> monthly<br>on your long-term<br><span class="hl-box">Mortgage</span> payment',
desc: 'Intelligent refinancing insights and negotiation support surface the best rates across 200+ lenders, cutting wasted interest and putting cash back in your pocket.',
cta: 'Refinance Now',
counterTarget: 1200,
counterPrefix: '$',
counterSuffix: '',
pic: 'https://images.unsplash.com/photo-1563013544-824ae1b704d3?auto=format&fit=crop&w=400&q=80',
pic1: 'https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?auto=format&fit=crop&w=500&q=80',
pic2: 'https://images.unsplash.com/photo-1579621970563-ebec7560ff3e?auto=format&fit=crop&w=500&q=80',
pic3: 'https://images.unsplash.com/photo-1554224155-6726b3ff858f?auto=format&fit=crop&w=800&q=80',
},
{
titleHtml: 'Unlock <span class="hl-yellow">Premium</span><br>access to your live<br><span class="hl-box">Portfolio</span> today',
desc: 'Real-time dashboards, rental yield forecasts, and concierge support from senior advisors who know the local market inside and out.',
cta: 'Go Premium',
counterTarget: 100,
counterPrefix: '',
counterSuffix: '+',
pic: 'https://images.unsplash.com/photo-1556761175-4b46a572b786?auto=format&fit=crop&w=400&q=80',
pic1: 'https://images.unsplash.com/photo-1556761175-5973dc0f32e7?auto=format&fit=crop&w=500&q=80',
pic2: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=500&q=80',
pic3: 'https://images.unsplash.com/photo-1521791136064-7986c2920216?auto=format&fit=crop&w=800&q=80',
},
];
let currentSlide = 0;
// -------------------------------------------------------------------
// BUILD LOADER PARTICLES
// -------------------------------------------------------------------
const particlesHost = document.getElementById('loader-particles');
const PARTICLE_COUNT = 12;
for (let i = 0; i < PARTICLE_COUNT; i++) {
const p = document.createElement('span');
const angle = (i / PARTICLE_COUNT) * Math.PI * 2;
const r = 60;
p.style.left = (110 + Math.cos(angle) * r) + 'px';
p.style.top = (110 + Math.sin(angle) * r) + 'px';
p.style.transform = 'translate(-50%, -50%)';
particlesHost.appendChild(p);
}
const particles = particlesHost.querySelectorAll('span');
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const loaderRingG = document.getElementById('loader-ring-g');
const loaderCore = document.getElementById('loader-core');
const magnetics = root.querySelectorAll('[data-magnetic]');
const titleEl = document.getElementById('title');
const descEl = document.getElementById('desc');
const ctaLabel = document.getElementById('cta-label');
const accentImg = document.getElementById('accent-img');
const pic1 = document.getElementById('pic-1');
const pic2 = document.getElementById('pic-2');
const pic3 = document.getElementById('pic-3');
const photoEls = root.querySelectorAll('[data-photo]');
const heroPic = root.querySelector('[data-pic]');
const controls = root.querySelector('[data-controls]');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
const slideCurrent = document.getElementById('slide-current');
const progressFill = document.getElementById('progress-fill');
const navBadge = document.getElementById('nav-badge');
const navBadgeText = navBadge.querySelector('.topnav__badge-text');
const logoMark = document.getElementById('logo-mark');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -15, opacity: 0 });
gsap.set(photoEls, { y: 30, opacity: 0, scale: 0.9 });
gsap.set(heroPic, { y: 20, opacity: 0 });
gsap.set(descEl, { y: 15, opacity: 0 });
gsap.set(controls, { y: 15, opacity: 0 });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Text ring rotates continuously
gsap.to(loaderRingG, {
rotation: 360,
duration: 10,
repeat: -1,
ease: 'none',
transformOrigin: '110px 110px',
});
// Core pulses
gsap.to(loaderCore, {
scale: 1.1,
duration: 1,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Particles orbit + twinkle
particles.forEach((p, i) => {
gsap.to(p, {
scale: 1.5,
opacity: 0.3,
duration: 0.7,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.08,
});
});
// Counter + labels
const p = { v: 0 };
loaderTl.to(p, {
v: 350000,
duration: 2.3,
ease: 'power2.out',
onUpdate: () => {
const val = Math.floor(p.v);
loaderCounter.textContent = '$' + val.toLocaleString('en-US');
if (p.v > 100000 && loaderLabel.textContent === 'BUILDING YOUR NEST') loaderLabel.textContent = 'APPROVING BUDGET';
if (p.v > 250000 && loaderLabel.textContent === 'APPROVING BUDGET') loaderLabel.textContent = 'ALMOST HOME';
if (p.v > 340000 && loaderLabel.textContent === 'ALMOST HOME') loaderLabel.textContent = 'READY';
},
});
// 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.2');
loaderTl.to(loader, {
opacity: 0, duration: 0.4, ease: 'power2.inOut',
}, '-=0.3');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// RENDER SLIDE CONTENT (with transition)
// -------------------------------------------------------------------
function renderSlide(idx, direction = 1) {
const slide = slides[idx];
const outTl = gsap.timeline();
// Out: title slides up, desc fades, photos exit
outTl.to(titleEl, {
y: -20 * direction,
opacity: 0,
duration: 0.35,
ease: 'power2.in',
}, 0);
outTl.to(descEl, {
y: -15,
opacity: 0,
duration: 0.3,
ease: 'power2.in',
}, 0);
outTl.to(photoEls, {
y: 20 * direction,
opacity: 0,
scale: 0.92,
duration: 0.4,
stagger: 0.04,
ease: 'power2.in',
}, 0);
outTl.to(ctaLabel, {
y: -15, opacity: 0, duration: 0.25, ease: 'power2.in',
}, 0);
outTl.to(heroPic, {
y: -15, opacity: 0, duration: 0.3, ease: 'power2.in',
}, 0);
outTl.call(() => {
// Update content
titleEl.innerHTML = slide.titleHtml;
descEl.textContent = slide.desc;
ctaLabel.textContent = slide.cta;
accentImg.src = slide.pic;
pic1.src = slide.pic1;
pic2.src = slide.pic2;
pic3.src = slide.pic3;
// Update slider progress
slideCurrent.textContent = String(idx + 1).padStart(2, '0');
gsap.to(progressFill, {
x: `${idx * 25}%`,
duration: 0.5,
ease: 'power2.out',
});
// Animate in
gsap.set(titleEl, { y: 30, opacity: 0 });
gsap.set(descEl, { y: 15, opacity: 0 });
gsap.set(photoEls, { y: -30 * direction, opacity: 0, scale: 0.9 });
gsap.set(ctaLabel, { y: 15, opacity: 0 });
gsap.set(heroPic, { y: 15, opacity: 0 });
gsap.to(titleEl, {
y: 0, opacity: 1,
duration: 0.7,
ease: 'power3.out',
});
// Stagger the highlighted spans for pop
const highlights = titleEl.querySelectorAll('.hl-yellow, .hl-box');
gsap.from(highlights, {
scale: 0.8,
duration: 0.6,
delay: 0.3,
stagger: 0.1,
ease: 'back.out(2)',
});
gsap.to(descEl, {
y: 0, opacity: 1,
duration: 0.6,
delay: 0.25,
ease: 'power3.out',
});
gsap.to(heroPic, {
y: 0, opacity: 1,
duration: 0.6,
delay: 0.2,
ease: 'power3.out',
});
gsap.to(ctaLabel, {
y: 0, opacity: 1,
duration: 0.5,
delay: 0.4,
ease: 'power3.out',
});
gsap.to(photoEls, {
y: 0, opacity: 1, scale: 1,
duration: 0.8,
stagger: 0.08,
delay: 0.2,
ease: 'back.out(1.4)',
});
// Attach title hover (since innerHTML was swapped)
attachTitleHover();
});
}
function attachTitleHover() {
const hlWords = titleEl.querySelectorAll('.hl-yellow, .hl-box');
hlWords.forEach((w) => {
w.addEventListener('mouseenter', () => {
gsap.to(w, { scale: 1.08, duration: 0.3, ease: 'back.out(2)' });
});
w.addEventListener('mouseleave', () => {
gsap.to(w, { scale: 1, duration: 0.4, ease: 'elastic.out(1, 0.4)' });
});
});
}
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
// Initial slide content
titleEl.innerHTML = slides[0].titleHtml;
descEl.textContent = slides[0].desc;
ctaLabel.textContent = slides[0].cta;
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Nav
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.05,
}, 0);
// Title fade in
gsap.set(titleEl, { y: 30, opacity: 0 });
tl.to(titleEl, { y: 0, opacity: 1, duration: 0.9 }, 0.3);
// Highlight pops
const highlights = titleEl.querySelectorAll('.hl-yellow, .hl-box');
tl.from(highlights, {
scale: 0.8,
duration: 0.7,
stagger: 0.1,
ease: 'back.out(2)',
}, 0.6);
// Lower content
tl.to(heroPic, { y: 0, opacity: 1, duration: 0.7 }, 0.9);
tl.to(descEl, { y: 0, opacity: 1, duration: 0.7 }, 1);
// Photos scatter in
tl.to(photoEls, {
y: 0, opacity: 1, scale: 1,
duration: 1,
stagger: 0.12,
ease: 'back.out(1.4)',
}, 0.7);
// Slider controls
tl.to(controls, { y: 0, opacity: 1, duration: 0.6 }, 1.4);
tl.call(() => {
attachTitleHover();
startContinuous();
enableInteractions();
}, null, 1.6);
}
// -------------------------------------------------------------------
// CONTINUOUS
// -------------------------------------------------------------------
function startContinuous() {
// Nav badge rotates
gsap.to(navBadgeText, {
rotation: 360,
duration: 12,
repeat: -1,
ease: 'none',
transformOrigin: 'center center',
});
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic ----
magnetics.forEach((el) => {
const strength = el.classList.contains('cta') ? 0.3
: el.classList.contains('slide-arrow') ? 0.4
: el.classList.contains('topnav__badge') ? 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 hover ----
const logoEl = root.querySelector('.topnav__logo');
logoEl.addEventListener('mouseenter', () => {
gsap.to(logoMark.querySelector('svg'), { y: -3, rotation: -5, duration: 0.4, ease: 'back.out(2)' });
});
logoEl.addEventListener('mouseleave', () => {
gsap.to(logoMark.querySelector('svg'), { y: 0, rotation: 0, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
});
// ---- Nav badge hover: speed up + core pop ----
navBadge.addEventListener('mouseenter', () => {
gsap.getTweensOf(navBadgeText).forEach(t => gsap.to(t, { timeScale: 4, duration: 0.3 }));
gsap.to(navBadge.querySelector('.topnav__badge-core'), {
scale: 1.2, rotation: 90,
duration: 0.3, ease: 'back.out(2)',
});
});
navBadge.addEventListener('mouseleave', () => {
gsap.getTweensOf(navBadgeText).forEach(t => gsap.to(t, { timeScale: 1, duration: 0.4 }));
gsap.to(navBadge.querySelector('.topnav__badge-core'), {
scale: 1, rotation: 0,
duration: 0.5, ease: 'elastic.out(1, 0.4)',
});
});
// ---- Photo hover ----
photoEls.forEach((photo) => {
photo.addEventListener('mouseenter', () => {
gsap.to(photo, {
scale: 1.04,
y: -8,
zIndex: 10,
duration: 0.4,
ease: 'back.out(2)',
});
});
photo.addEventListener('mouseleave', () => {
gsap.to(photo, {
scale: 1, y: 0, zIndex: 1,
duration: 0.5,
ease: 'elastic.out(1, 0.4)',
});
});
});
// ---- Slider arrows ----
const advance = (dir) => {
const next = (currentSlide + dir + slides.length) % slides.length;
currentSlide = next;
renderSlide(next, dir);
};
nextBtn.addEventListener('click', () => advance(1));
prevBtn.addEventListener('click', () => advance(-1));
// Keyboard
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') advance(-1);
if (e.key === 'ArrowRight') advance(1);
});
// Auto advance
let autoInterval = setInterval(() => advance(1), 7000);
controls.addEventListener('mouseenter', () => clearInterval(autoInterval));
controls.addEventListener('mouseleave', () => {
autoInterval = setInterval(() => advance(1), 7000);
});
// ---- CTA click particles ----
const cta = root.querySelector('[data-cta]');
cta.addEventListener('click', (e) => {
e.preventDefault();
const rect = cta.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
for (let i = 0; i < 8; i++) {
const s = document.createElement('span');
s.style.cssText = `position:fixed;left:${cx}px;top:${cy}px;width:6px;height:6px;background:#fcc83d;border-radius:50%;pointer-events:none;z-index:200;transform:translate(-50%,-50%);box-shadow:0 0 8px #fcc83d;`;
document.body.appendChild(s);
const a = (i / 8) * Math.PI * 2;
gsap.fromTo(s,
{ x: 0, y: 0, opacity: 1 },
{
x: Math.cos(a) * 80,
y: Math.sin(a) * 80,
opacity: 0,
scale: 0.3,
duration: 0.7,
ease: 'power2.out',
onComplete: () => s.remove(),
}
);
}
});
// ---- Photos parallax ----
let mx = 0, my = 0, cmx = 0, cmy = 0;
root.addEventListener('mousemove', (e) => {
const r = root.getBoundingClientRect();
mx = ((e.clientX - r.left) / r.width - 0.5) * 2;
my = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
root.addEventListener('mouseleave', () => { mx = 0; my = 0; });
gsap.ticker.add(() => {
cmx += (mx - cmx) * 0.05;
cmy += (my - cmy) * 0.05;
photoEls.forEach((photo, i) => {
if (photo.matches(':hover')) return;
const depth = 0.3 + i * 0.15;
gsap.set(photo, {
x: cmx * 20 * depth,
y: cmy * 15 * depth,
});
});
});
}
})();