JSbanner-saas-tech.js— GSAP animation timeline
(() => {
const root = document.getElementById('saas');
if (!root) return;
// -------------------------------------------------------------------
// GENERATE SONAR PULSES (loader)
// -------------------------------------------------------------------
const pulses = document.getElementById('pulses');
const PULSE_COUNT = 4;
for (let i = 0; i < PULSE_COUNT; i++) pulses.appendChild(document.createElement('span'));
const pulseEls = pulses.querySelectorAll('span');
// -------------------------------------------------------------------
// SPLIT TITLE INTO CHARS
// -------------------------------------------------------------------
const titleLines = root.querySelectorAll('[data-title-line]');
titleLines.forEach((line) => {
const text = line.textContent;
line.textContent = '';
[...text].forEach((ch) => {
const span = document.createElement('span');
span.className = 'hero__char';
if (ch === ' ') { span.classList.add('is-space'); span.innerHTML = ' '; }
else span.textContent = ch;
line.appendChild(span);
});
});
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const sonar = document.getElementById('sonar');
const magnetics = root.querySelectorAll('[data-magnetic]');
const titleChars = root.querySelectorAll('.hero__char');
const fades = root.querySelectorAll('[data-fade]');
const eyebrow = root.querySelector('[data-eyebrow]');
const eyebrowLine = root.querySelector('.hero__eyebrow-line');
const photo = document.getElementById('photo');
const photoShape = root.querySelector('[data-shape]');
const badge = document.getElementById('badge');
const badgeCircleSvg = badge.querySelector('svg:first-child');
const stripes = root.querySelector('[data-stripes]');
const curves = root.querySelectorAll('.curves path');
const curveDots = root.querySelectorAll('.curves circle');
const navLogoMark = document.getElementById('nav-logo-mark');
const phoneRing = document.getElementById('phone-ring');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -20, opacity: 0 });
gsap.set(eyebrow, { x: -20, opacity: 0 });
gsap.set(eyebrowLine, { scaleX: 0 });
gsap.set(titleChars, { yPercent: 110, opacity: 0 });
gsap.set(fades, { y: 24, opacity: 0 });
gsap.set(photoShape, { scale: 0.85, opacity: 0 });
gsap.set(photo, { scale: 0.92, opacity: 0, y: 30 });
gsap.set(badge, { scale: 0, opacity: 0, rotation: -90 });
gsap.set(stripes, { scale: 0.3, opacity: 0, rotation: -30 });
gsap.set(curveDots, { scale: 0, opacity: 0, transformOrigin: 'center center' });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Sonar pulses: continuously expanding rings
const pulseTweens = [];
pulseEls.forEach((el, i) => {
const tween = gsap.fromTo(el,
{ scale: 0.6, opacity: 0.6 },
{
scale: 3.5,
opacity: 0,
duration: 2,
repeat: -1,
ease: 'power1.out',
delay: i * 0.5,
}
);
pulseTweens.push(tween);
});
// Sonar container subtle breathe
gsap.to(sonar, {
scale: 1.04,
duration: 1.2,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Counter + labels
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2.3,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = String(Math.floor(p.v)).padStart(2, '0') + '%';
if (p.v > 25 && loaderLabel.textContent === 'CONNECTING') loaderLabel.textContent = 'OPTIMIZING';
if (p.v > 60 && loaderLabel.textContent === 'OPTIMIZING') loaderLabel.textContent = 'ALMOST READY';
if (p.v > 95 && loaderLabel.textContent === 'ALMOST READY') loaderLabel.textContent = 'SIGNAL ACQUIRED';
},
});
// Final big burst: stop loops, let last pulse fly out
loaderTl.call(() => {
pulseTweens.forEach(t => t.pause());
}, null, '+=0.1');
loaderTl.to(pulseEls, {
scale: 8,
opacity: 0,
duration: 0.9,
ease: 'power2.out',
stagger: 0.05,
}, '+=0.05');
// Exit
loaderTl.to([loaderCounter, loaderLabel], {
y: -10, opacity: 0, duration: 0.3, stagger: 0.05,
}, '-=0.5');
loaderTl.to(sonar, {
scale: 1.5, opacity: 0, duration: 0.6, ease: 'power3.in',
}, '-=0.4');
loaderTl.to(loader, {
opacity: 0, duration: 0.4, ease: 'power2.inOut',
}, '-=0.2');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Decorative curves draw in
tl.to(curves, {
strokeDashoffset: 0,
duration: 1.8,
stagger: 0.12,
ease: 'power2.inOut',
}, 0);
tl.to(curveDots, {
scale: 1, opacity: 1,
duration: 0.5,
stagger: 0.1,
ease: 'back.out(2)',
}, 0.5);
// Nav pill
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.05,
}, 0.2);
// Eyebrow + line
tl.to(eyebrow, {
x: 0, opacity: 1,
duration: 0.6,
}, 0.6);
tl.to(eyebrowLine, {
scaleX: 1,
duration: 0.7,
ease: 'power4.inOut',
}, 0.7);
// Title chars
tl.to(titleChars, {
yPercent: 0, opacity: 1,
duration: 1.1,
stagger: 0.02,
ease: 'expo.out',
}, 0.8);
// Description + CTA row
tl.to(fades, {
y: 0, opacity: 1,
duration: 0.7,
stagger: 0.15,
}, 1.2);
// Photo shape + photo
tl.to(photoShape, {
scale: 1, opacity: 1,
duration: 1.2,
ease: 'power4.out',
}, 0.5);
tl.to(photo, {
scale: 1, opacity: 1, y: 0,
duration: 1.3,
ease: 'power4.out',
}, 0.7);
// Badge
tl.to(badge, {
scale: 1, opacity: 1, rotation: 0,
duration: 0.9,
ease: 'back.out(1.6)',
}, 1.2);
// Stripes
tl.to(stripes, {
scale: 1, opacity: 0.85, rotation: 0,
duration: 0.8,
ease: 'back.out(1.6)',
}, 1.35);
tl.call(startContinuous, null, 1.6);
tl.call(enableInteractions, null, 1.6);
}
// -------------------------------------------------------------------
// CONTINUOUS LOOPS
// -------------------------------------------------------------------
let badgeRot = null;
function startContinuous() {
// Badge text rotates continuously
badgeRot = gsap.to(badgeCircleSvg, {
rotation: 360,
duration: 18,
repeat: -1,
ease: 'none',
transformOrigin: 'center center',
});
// Photo subtle breathing (baked into transform so parallax can add)
gsap.to(photo, {
y: -8,
duration: 3,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Phone ring pulse
gsap.to(phoneRing, {
scale: 1.15,
opacity: 0,
duration: 1.4,
repeat: -1,
ease: 'power1.out',
transformOrigin: 'center center',
});
// Stripes slow rotation
gsap.to(stripes, {
rotation: '+=360',
duration: 28,
repeat: -1,
ease: 'none',
transformOrigin: 'center center',
});
// Logo sonar arcs subtle animation (stagger opacity)
const logoArcs = navLogoMark.querySelectorAll('path');
logoArcs.forEach((arc, i) => {
gsap.fromTo(arc,
{ opacity: 0.3 },
{
opacity: 1,
duration: 0.9,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.2,
}
);
});
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic ----
magnetics.forEach((el) => {
const strength = el.classList.contains('topnav__cta') ? 0.35
: el.classList.contains('btn-primary') ? 0.3
: el.classList.contains('phone') ? 0.25
: 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 sonar burst on hover ----
const logo = root.querySelector('.topnav__logo');
logo.addEventListener('mouseenter', () => {
const arcs = navLogoMark.querySelectorAll('path');
gsap.fromTo(arcs,
{ scale: 0.8 },
{
scale: 1.15,
duration: 0.25,
yoyo: true,
repeat: 1,
stagger: 0.08,
ease: 'sine.inOut',
transformOrigin: 'center',
}
);
});
// ---- Title char proximity + hover ----
const hero = root.querySelector('.hero');
let tmx = -9999, tmy = -9999;
hero.addEventListener('mousemove', (e) => { tmx = e.clientX; tmy = e.clientY; });
hero.addEventListener('mouseleave', () => {
tmx = -9999; tmy = -9999;
titleChars.forEach((c) => gsap.to(c, { y: 0, duration: 0.5, ease: 'power3.out' }));
});
gsap.ticker.add(() => {
if (tmx < 0) return;
titleChars.forEach((c) => {
const r = c.getBoundingClientRect();
if (r.width === 0) return;
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = tmx - cx, dy = tmy - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 140) gsap.set(c, { y: -(1 - dist / 140) * 14 });
else gsap.set(c, { y: 0 });
});
});
titleChars.forEach((c) => {
c.addEventListener('mouseenter', () => gsap.to(c, { color: '#2563eb', duration: 0.2 }));
c.addEventListener('mouseleave', () => gsap.to(c, { color: '#0f172a', duration: 0.4 }));
});
// ---- Badge: speed up rotation on hover ----
badge.addEventListener('mouseenter', () => {
if (badgeRot) gsap.to(badgeRot, { timeScale: 5, duration: 0.5 });
gsap.to(badge, { scale: 1.08, duration: 0.4, ease: 'back.out(2)' });
});
badge.addEventListener('mouseleave', () => {
if (badgeRot) gsap.to(badgeRot, { timeScale: 1, duration: 0.6 });
gsap.to(badge, { scale: 1, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
});
// ---- Phone: tilt-shake on hover (like ringing) ----
const phoneEl = root.querySelector('[data-phone]');
const phoneIconSvg = phoneEl.querySelector('.phone__icon svg');
phoneEl.addEventListener('mouseenter', () => {
gsap.fromTo(phoneIconSvg,
{ rotation: 0 },
{
rotation: 15,
duration: 0.08,
yoyo: true,
repeat: 5,
ease: 'sine.inOut',
transformOrigin: 'center center',
}
);
});
// ---- Photo 3D tilt parallax ----
const right = root.querySelector('.hero__right');
let rx = 0, ry = 0, trx = 0, try_ = 0;
right.addEventListener('mousemove', (e) => {
const r = right.getBoundingClientRect();
trx = ((e.clientX - r.left) / r.width - 0.5) * 2;
try_ = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
right.addEventListener('mouseleave', () => { trx = 0; try_ = 0; });
gsap.ticker.add(() => {
rx += (trx - rx) * 0.08;
ry += (try_ - ry) * 0.08;
gsap.set(photo, {
rotationY: rx * 6,
rotationX: -ry * 4,
x: rx * 10,
transformPerspective: 1000,
transformOrigin: 'center',
});
gsap.set(photoShape, {
x: -rx * 6,
y: -ry * 4,
});
gsap.set(badge, {
x: rx * 14,
y: ry * 10,
});
gsap.set(stripes, {
x: rx * 18,
y: ry * 12,
});
});
// ---- Decorative curves respond to cursor (shift) ----
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; });
const curvesSvg = root.querySelector('.curves');
gsap.ticker.add(() => {
gcX += (gX - gcX) * 0.04;
gcY += (gY - gcY) * 0.04;
gsap.set(curvesSvg, { x: gcX * 20, y: gcY * 14 });
});
}
})();