JSbanner-axis.jsโ GSAP animation timeline
(() => {
const root = document.getElementById('axis');
if (!root) return;
// -------------------------------------------------------------------
// WRAP TITLE WORDS IN INNER SPAN (for reveal + hover color)
// -------------------------------------------------------------------
const titleWords = root.querySelectorAll('[data-title-word]');
titleWords.forEach((w) => {
const text = w.textContent;
w.textContent = '';
const inner = document.createElement('span');
inner.className = 'card__word-inner';
inner.textContent = text;
w.appendChild(inner);
});
const wordInners = root.querySelectorAll('.card__word-inner');
// -------------------------------------------------------------------
// BUILD LOADER TICK MARKS
// -------------------------------------------------------------------
const ticksG = document.getElementById('loader-ticks');
const TICK_COUNT = 24;
for (let i = 0; i < TICK_COUNT; i++) {
const angle = (i / TICK_COUNT) * 360;
const tick = document.createElementNS('http://www.w3.org/2000/svg', 'line');
const isLarge = i % 6 === 0;
const rad = (angle - 90) * Math.PI / 180;
const r1 = isLarge ? 73 : 77;
const r2 = 85;
tick.setAttribute('x1', 100 + Math.cos(rad) * r1);
tick.setAttribute('y1', 100 + Math.sin(rad) * r1);
tick.setAttribute('x2', 100 + Math.cos(rad) * r2);
tick.setAttribute('y2', 100 + Math.sin(rad) * r2);
tick.setAttribute('stroke-width', isLarge ? 2 : 1);
tick.setAttribute('opacity', 0);
ticksG.appendChild(tick);
}
const tickEls = ticksG.querySelectorAll('line');
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const loaderOuter = document.getElementById('loader-outer');
const loaderNeedle = document.getElementById('loader-needle');
const axisV = document.getElementById('axis-v');
const axisH = document.getElementById('axis-h');
const magnetics = root.querySelectorAll('[data-magnetic]');
const fades = root.querySelectorAll('[data-fade]');
const card = root.querySelector('[data-card]');
const photo = document.getElementById('photo');
const photoImg = photo.querySelector('img');
const arc = root.querySelector('[data-arc]');
const logoMark = document.getElementById('logo-mark');
const dots = root.querySelectorAll('.dots span');
const dotsContainer = root.querySelector('[data-dots]');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
const playRing = root.querySelector('.btn-play-ring');
const socialBtns = root.querySelectorAll('[data-social]');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -15, opacity: 0 });
gsap.set(fades, { y: 20, opacity: 0 });
gsap.set(wordInners, { yPercent: 110, opacity: 0 });
gsap.set(card, { x: -60, opacity: 0, scale: 0.96 });
gsap.set(photo, { opacity: 0 });
gsap.set(photoImg, { scale: 1.15 });
gsap.set(arc, { xPercent: 50, opacity: 0 });
gsap.set(socialBtns, { scale: 0, opacity: 0 });
gsap.set(dotsContainer, { y: 20, opacity: 0 });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Outer ring draws
loaderTl.to(loaderOuter, {
strokeDashoffset: 0,
duration: 1.2,
ease: 'power2.inOut',
}, 0);
// Axes draw
loaderTl.to([axisV, axisH], {
strokeDashoffset: 0,
duration: 0.8,
stagger: 0.1,
ease: 'power2.out',
}, 0.4);
// Ticks appear sequentially
loaderTl.to(tickEls, {
opacity: 1,
duration: 0.03,
stagger: 0.025,
}, 0.6);
// Needle rotates continuously (like compass seeking)
gsap.to(loaderNeedle, {
rotation: 360,
duration: 2.2,
repeat: -1,
ease: 'none',
});
// Counter + labels
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = Math.floor(p.v) + '%';
if (p.v > 30 && loaderLabel.textContent === 'CALIBRATING') loaderLabel.textContent = 'ALIGNING AXES';
if (p.v > 60 && loaderLabel.textContent === 'ALIGNING AXES') loaderLabel.textContent = 'FINDING TRUE NORTH';
if (p.v > 95 && loaderLabel.textContent === 'FINDING TRUE NORTH') loaderLabel.textContent = 'READY';
},
}, 0.2);
// Exit: dial scales + fades
loaderTl.to([loaderCounter, loaderLabel], {
y: -10, opacity: 0, duration: 0.3, stagger: 0.05,
}, '+=0.3');
loaderTl.to('.loader__dial', {
scale: 1.2, opacity: 0, duration: 0.6, ease: 'power3.in',
}, '-=0.3');
loaderTl.to(loader, {
opacity: 0, duration: 0.4, ease: 'power2.inOut',
}, '-=0.3');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Background arc slides in
tl.to(arc, {
xPercent: 0, opacity: 1,
duration: 1.2,
ease: 'power4.out',
}, 0);
// Photo fades + zoom settle
tl.to(photo, { opacity: 1, duration: 1 }, 0.3);
tl.to(photoImg, {
scale: 1,
duration: 1.6,
ease: 'power3.out',
}, 0.3);
// Nav
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.05,
}, 0.4);
// Social buttons pop
tl.to(socialBtns, {
scale: 1, opacity: 1,
duration: 0.6,
stagger: 0.08,
ease: 'back.out(1.8)',
}, 0.7);
// Card slides in from left
tl.to(card, {
x: 0, opacity: 1, scale: 1,
duration: 1.1,
ease: 'power4.out',
}, 0.6);
// Title words reveal
tl.to(wordInners, {
yPercent: 0, opacity: 1,
duration: 0.9,
stagger: 0.1,
ease: 'expo.out',
}, 0.95);
// Description + CTA
tl.to(fades, {
y: 0, opacity: 1,
duration: 0.7,
stagger: 0.15,
}, 1.3);
// Dots
tl.to(dotsContainer, { y: 0, opacity: 1, duration: 0.6 }, 1.6);
tl.call(startContinuous, null, 1.8);
tl.call(enableInteractions, null, 1.8);
}
// -------------------------------------------------------------------
// CONTINUOUS
// -------------------------------------------------------------------
function startContinuous() {
// Play ring pulse
gsap.to(playRing, {
scale: 1.25,
opacity: 0,
duration: 1.6,
repeat: -1,
ease: 'power1.out',
transformOrigin: 'center',
});
// Active dot breath
const activeDot = root.querySelector('.dots span.is-active');
if (activeDot) gsap.to(activeDot, {
scale: 1.35,
duration: 1,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic ----
magnetics.forEach((el) => {
const strength = el.classList.contains('btn-dark') ? 0.3
: el.classList.contains('btn-play') ? 0.4
: el.classList.contains('nav-arrow') ? 0.45
: 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: icon rotates on hover ----
const logoEl = root.querySelector('.topnav__logo');
logoEl.addEventListener('mouseenter', () => {
gsap.to(logoMark.querySelector('svg'), { rotation: 360, duration: 0.7, ease: 'power3.inOut' });
});
logoEl.addEventListener('mouseleave', () => {
gsap.set(logoMark.querySelector('svg'), { rotation: 0 });
});
// ---- Title words hover: accent color ----
wordInners.forEach((inner) => {
const isAccent = inner.closest('.card__word--accent');
inner.addEventListener('mouseenter', () => {
gsap.to(inner, { color: isAccent ? '#1a1a2e' : '#3452e8', duration: 0.2 });
});
inner.addEventListener('mouseleave', () => {
gsap.to(inner, { color: isAccent ? '#3452e8' : '#1a1a2e', duration: 0.4 });
});
});
// ---- Play button: click = pulse wave ----
const playBtn = root.querySelector('[data-play]');
playBtn.addEventListener('click', (e) => {
e.preventDefault();
gsap.fromTo(playBtn,
{ scale: 1 },
{ scale: 0.9, duration: 0.15, yoyo: true, repeat: 1, ease: 'back.out(2)' }
);
// Burst rings
for (let i = 0; i < 3; i++) {
const r = document.createElement('span');
r.style.cssText = `position:absolute;inset:0;border:2px solid #3452e8;border-radius:50%;pointer-events:none;`;
playBtn.appendChild(r);
gsap.fromTo(r,
{ scale: 1, opacity: 0.7 },
{
scale: 2.2, opacity: 0,
duration: 0.8,
delay: i * 0.1,
ease: 'power2.out',
onComplete: () => r.remove(),
}
);
}
});
// ---- Slide content (4 unique slides) ----
const slides = [
{
accent: 'Axis',
words: ['Business', 'Template'],
desc: 'This is our dedicated team who work day-in and day-out together to bring our clients the most amazing projects for a digitally connected world.',
image: 'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1400&q=80',
},
{
accent: 'Creative',
words: ['Marketing', 'Agency'],
desc: 'We craft data-driven campaigns that tell meaningful stories, reach the right audience, and turn casual browsers into lifelong customers.',
image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&w=1400&q=80',
},
{
accent: 'Strategic',
words: ['Partners', 'Worldwide'],
desc: 'A network of senior consultants, designers, and engineers ready to plug into your team across time zones and scale your next big move.',
image: 'https://images.unsplash.com/photo-1600880292203-757bb62b4baf?auto=format&fit=crop&w=1400&q=80',
},
{
accent: 'Innovative',
words: ['Digital', 'Solutions'],
desc: 'From AI workflows to real-time dashboards, we build pragmatic software that ships fast, scales cleanly, and actually moves the needle.',
image: 'https://images.unsplash.com/photo-1556761175-5973dc0f32e7?auto=format&fit=crop&w=1400&q=80',
},
];
const titleEl = root.querySelector('.card__title');
const descEl = root.querySelector('.card__desc');
let currentSlide = 0; // initial (1st slide is-active per HTML)
function renderSlide(idx, direction = 1) {
const slide = slides[idx];
const oldInners = titleEl.querySelectorAll('.card__word-inner');
const outTl = gsap.timeline();
// Slide out old content
outTl.to(oldInners, {
yPercent: -110,
opacity: 0,
duration: 0.35,
stagger: 0.04,
ease: 'power2.in',
}, 0);
outTl.to(descEl, {
y: -15,
opacity: 0,
duration: 0.3,
ease: 'power2.in',
}, 0);
outTl.to(photoImg, {
scale: 1.15,
opacity: 0.4,
duration: 0.45,
ease: 'power2.in',
}, 0);
outTl.call(() => {
// Rebuild title HTML with new content
titleEl.innerHTML = `
<span class="card__word card__word--accent"><span class="card__word-inner">${slide.accent}</span></span>
<span class="card__word"><span class="card__word-inner">${slide.words[0]}</span></span>
<span class="card__word"><span class="card__word-inner">${slide.words[1]}</span></span>
`;
descEl.textContent = slide.desc;
photoImg.src = slide.image;
// Attach hover handlers on the new words
const newInners = titleEl.querySelectorAll('.card__word-inner');
newInners.forEach((inner) => {
const isAccent = inner.closest('.card__word--accent');
inner.addEventListener('mouseenter', () => {
gsap.to(inner, { color: isAccent ? '#1a1a2e' : '#3452e8', duration: 0.2 });
});
inner.addEventListener('mouseleave', () => {
gsap.to(inner, { color: isAccent ? '#3452e8' : '#1a1a2e', duration: 0.4 });
});
});
// Animate in new content
gsap.set(newInners, { yPercent: 110, opacity: 0 });
gsap.set(descEl, { y: 15, opacity: 0 });
gsap.to(newInners, {
yPercent: 0, opacity: 1,
duration: 0.7,
stagger: 0.08,
ease: 'power3.out',
});
gsap.to(descEl, {
y: 0, opacity: 1,
duration: 0.6,
delay: 0.15,
ease: 'power3.out',
});
gsap.to(photoImg, {
scale: 1.03, opacity: 1,
duration: 0.9,
ease: 'power3.out',
});
});
// Arc + card flash on transition
gsap.fromTo(arc,
{ x: 30 * direction },
{ x: 0, duration: 0.9, ease: 'power4.out' }
);
gsap.fromTo(card,
{ x: -20 * direction },
{ x: 0, duration: 0.9, ease: 'power4.out' }
);
}
// ---- Dots ----
dots.forEach((dot, i) => {
dot.addEventListener('click', () => {
if (i === currentSlide) return;
const direction = i > currentSlide ? 1 : -1;
// Handle wrap (far end โ first slide reads as forward visually)
dots.forEach(d => d.classList.remove('is-active'));
dot.classList.add('is-active');
// Re-apply active-dot breathing to the new active
const newActive = root.querySelector('.dots span.is-active');
gsap.killTweensOf(dots);
gsap.set(dots, { scale: 1 });
if (newActive) gsap.to(newActive, {
scale: 1.35,
duration: 1,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
currentSlide = i;
renderSlide(i, direction);
});
});
// ---- Nav arrows: advance dots ----
const advance = (dir) => {
const next = (currentSlide + dir + dots.length) % dots.length;
dots[next].click();
};
nextBtn.addEventListener('click', () => advance(1));
prevBtn.addEventListener('click', () => advance(-1));
// Keyboard nav
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') advance(-1);
if (e.key === 'ArrowRight') advance(1);
});
// ---- Photo parallax + tilt ----
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;
gsap.set(photoImg, {
scale: 1.03,
x: cmx * 18,
y: cmy * 12,
});
gsap.set(arc, {
x: cmx * 14,
y: cmy * 10,
});
gsap.set(card, {
x: cmx * 6,
y: cmy * 4,
});
});
}
})();