JSbanner-counsel.js— GSAP animation timeline
(() => {
const root = document.getElementById('counsel');
if (!root) return;
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const scalesBeam = document.getElementById('scales-beam');
const magnetics = root.querySelectorAll('[data-magnetic]');
const titleEms = root.querySelectorAll('[data-title-line] em, [data-title-line] b');
const fades = root.querySelectorAll('[data-fade]');
const photos = root.querySelectorAll('[data-photo]');
const badges = root.querySelectorAll('[data-badge]');
const waves = root.querySelectorAll('.waves path');
const logoMark = document.getElementById('logo-mark');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -15, opacity: 0 });
gsap.set(fades, { y: 20, opacity: 0 });
gsap.set(titleEms, { yPercent: 110, opacity: 0 });
gsap.set(photos, { scale: 0.6, opacity: 0 });
gsap.set(badges, { scale: 0, opacity: 0, rotation: -15 });
// -------------------------------------------------------------------
// LOADER TIMELINE — Scales of justice
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Scales oscillate back and forth, gradually settling
const swings = [
{ rot: 15, duration: 0.9 },
{ rot: -12, duration: 0.8 },
{ rot: 9, duration: 0.7 },
{ rot: -6, duration: 0.6 },
{ rot: 3, duration: 0.5 },
{ rot: 0, duration: 0.4 },
];
swings.forEach((s) => {
loaderTl.to(scalesBeam, {
rotation: s.rot,
duration: s.duration,
ease: 'sine.inOut',
});
});
// Counter + labels (parallel with swings)
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 3.5,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = Math.floor(p.v) + '%';
if (p.v > 30 && loaderLabel.textContent === 'BUILDING TRUST') loaderLabel.textContent = 'REVIEWING BRIEFS';
if (p.v > 60 && loaderLabel.textContent === 'REVIEWING BRIEFS') loaderLabel.textContent = 'FINDING BALANCE';
if (p.v > 95 && loaderLabel.textContent === 'FINDING BALANCE') loaderLabel.textContent = 'READY TO COUNSEL';
},
}, 0);
// Exit
loaderTl.to([loaderCounter, loaderLabel], {
y: -10, opacity: 0, duration: 0.3, stagger: 0.05,
}, '+=0.2');
loaderTl.to('.loader__scales', {
y: -30, scale: 1.1, opacity: 0, duration: 0.6, ease: 'power3.in',
}, '-=0.3');
loaderTl.to(loader, {
opacity: 0, duration: 0.5, ease: 'power2.inOut',
}, '-=0.3');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Background waves draw in
tl.to(waves, {
strokeDashoffset: 0,
duration: 2.5,
stagger: 0.12,
ease: 'power2.inOut',
}, 0);
// Top nav
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.06,
}, 0.2);
// Photos scale in (stagger)
tl.to(photos, {
scale: 1, opacity: 1,
duration: 1,
stagger: 0.15,
ease: 'back.out(1.6)',
}, 0.5);
// Badges pop after photos
tl.to(badges, {
scale: 1, opacity: 1, rotation: 0,
duration: 0.8,
stagger: 0.12,
ease: 'back.out(2)',
}, 1);
// Title emphases rise
tl.to(titleEms, {
yPercent: 0, opacity: 1,
duration: 1.1,
stagger: 0.12,
ease: 'expo.out',
}, 0.9);
// Desc + CTA
tl.to(fades, {
y: 0, opacity: 1,
duration: 0.7,
stagger: 0.15,
}, 1.5);
tl.call(startContinuous, null, 1.8);
tl.call(enableInteractions, null, 1.8);
}
// -------------------------------------------------------------------
// CONTINUOUS
// -------------------------------------------------------------------
function startContinuous() {
// Badges gentle float
badges.forEach((badge, i) => {
gsap.to(badge, {
y: '-=8',
rotation: i % 2 === 0 ? 2 : -2,
duration: 2 + i * 0.4,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
});
// Phone icon subtle pulse
const phoneIcon = root.querySelector('.topnav__phone-icon');
gsap.to(phoneIcon, {
scale: 1.08,
duration: 1.2,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic ----
magnetics.forEach((el) => {
const strength = el.classList.contains('cta') ? 0.35
: el.classList.contains('topnav__home') ? 0.4
: el.classList.contains('topnav__logo') ? 0.18
: 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: icon rotates on hover ----
const logoEl = root.querySelector('.topnav__logo');
logoEl.addEventListener('mouseenter', () => {
gsap.to(logoMark.querySelector('svg'), { rotation: 360, duration: 0.8, ease: 'power3.inOut' });
});
logoEl.addEventListener('mouseleave', () => {
gsap.set(logoMark.querySelector('svg'), { rotation: 0 });
});
// ---- Title emphasis hover: lift + color shift ----
titleEms.forEach((em) => {
em.addEventListener('mouseenter', () => {
gsap.to(em, {
y: -8,
duration: 0.35,
ease: 'back.out(2)',
});
});
em.addEventListener('mouseleave', () => {
gsap.to(em, {
y: 0,
duration: 0.5,
ease: 'elastic.out(1, 0.4)',
});
});
});
// ---- Photos: parallax + hover tilt ----
photos.forEach((photo) => {
photo.addEventListener('mouseenter', () => {
gsap.to(photo, {
scale: 1.06,
duration: 0.4,
ease: 'back.out(2)',
zIndex: 10,
});
});
photo.addEventListener('mouseleave', () => {
gsap.to(photo, {
scale: 1,
duration: 0.5,
ease: 'elastic.out(1, 0.4)',
zIndex: 2,
});
});
});
// ---- Badges: click wiggles ----
badges.forEach((badge) => {
badge.addEventListener('mouseenter', () => {
gsap.fromTo(badge,
{ rotation: -5 },
{ rotation: 5, duration: 0.15, yoyo: true, repeat: 3, ease: 'sine.inOut',
onComplete: () => gsap.to(badge, { rotation: 0, duration: 0.3 })
}
);
});
});
// ---- Phone: number scrambles on hover ----
const phoneEl = root.querySelector('[data-phone]');
const phoneNumEl = document.getElementById('phone-number');
const phoneFinal = phoneNumEl.textContent;
phoneEl.addEventListener('mouseenter', () => {
let t = 0;
const scramble = () => {
t++;
if (t >= 7) {
phoneNumEl.textContent = phoneFinal;
return;
}
phoneNumEl.textContent = phoneFinal.split('').map((ch) => {
if (/\d/.test(ch)) return String(Math.floor(Math.random() * 10));
return ch;
}).join('');
setTimeout(scramble, 40);
};
scramble();
// Also wobble phone icon
const icon = phoneEl.querySelector('.topnav__phone-icon svg');
gsap.fromTo(icon,
{ rotation: 0 },
{ rotation: 15, duration: 0.08, yoyo: true, repeat: 4, ease: 'sine.inOut',
transformOrigin: 'center' }
);
});
// ---- Photo parallax on mouse ----
const photosScene = document.querySelector('.photos');
let mx = 0, my = 0, cmx = 0, cmy = 0;
photosScene.addEventListener('mousemove', (e) => {
const r = photosScene.getBoundingClientRect();
mx = ((e.clientX - r.left) / r.width - 0.5) * 2;
my = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
photosScene.addEventListener('mouseleave', () => { mx = 0; my = 0; });
gsap.ticker.add(() => {
cmx += (mx - cmx) * 0.05;
cmy += (my - cmy) * 0.05;
photos.forEach((photo, i) => {
if (photo.matches(':hover')) return;
const depth = 0.4 + i * 0.15;
gsap.set(photo, {
x: cmx * 20 * depth,
y: cmy * 14 * depth,
});
});
badges.forEach((badge, i) => {
gsap.set(badge, {
x: cmx * (15 + i * 5),
y: cmy * (10 + i * 3),
});
});
});
// ---- Waves drift subtly with overall cursor ----
const wavesSvg = root.querySelector('.waves');
let gx = 0, gy = 0, gcx = 0, gcy = 0;
root.addEventListener('mousemove', (e) => {
const r = root.getBoundingClientRect();
gx = ((e.clientX - r.left) / r.width - 0.5) * 2;
gy = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
root.addEventListener('mouseleave', () => { gx = 0; gy = 0; });
gsap.ticker.add(() => {
gcx += (gx - gcx) * 0.04;
gcy += (gy - gcy) * 0.04;
gsap.set(wavesSvg, { x: gcx * 25, y: gcy * 18 });
});
}
})();