JSbanner-supplements-store.jsโ GSAP animation timeline
(() => {
const root = document.getElementById('store');
if (!root) return;
// -------------------------------------------------------------------
// 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);
});
});
// -------------------------------------------------------------------
// BUILD FALLING PARTICLES (loader)
// -------------------------------------------------------------------
const particles = document.getElementById('loader-particles');
const PARTICLE_COUNT = 18;
for (let i = 0; i < PARTICLE_COUNT; i++) particles.appendChild(document.createElement('span'));
const particleEls = particles.querySelectorAll('span');
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const loaderScoop = document.getElementById('loader-scoop');
const loaderFill = document.getElementById('loader-fill');
const loaderFoam = document.getElementById('loader-foam');
const magnetics = root.querySelectorAll('[data-magnetic]');
const titleChars = root.querySelectorAll('.hero__char');
const fades = root.querySelectorAll('[data-fade]');
const bottles = root.querySelectorAll('[data-bottle]');
const leaves = root.querySelectorAll('[data-leaf]');
const scoop = root.querySelector('[data-scoop]');
const bowl = root.querySelector('[data-bowl]');
const cta = root.querySelector('[data-cta]');
const ctaGlow = root.querySelector('.hero__cta-glow');
const arch = root.querySelector('[data-arch]');
const partners = root.querySelectorAll('[data-partner]');
const cartEl = root.querySelector('[data-cart]');
const cartCount = document.getElementById('cart-count');
// Bottle base transforms (so parallax preserves the rotation/scale)
const bottleBaseRot = [-8, 0, 8];
const bottleBaseScale = [1, 1.15, 1];
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -20, opacity: 0 });
gsap.set(fades, { y: 20, opacity: 0 });
gsap.set(titleChars, { yPercent: 110, opacity: 0 });
gsap.set(cta, { y: 30, opacity: 0, scale: 0.9 });
gsap.set(arch, { scale: 0.2, opacity: 0, transformOrigin: 'center bottom' });
gsap.set(bottles[0], { y: 120, opacity: 0, rotation: -8 });
gsap.set(bottles[1], { y: 140, opacity: 0, rotation: 0, scale: 1.15 });
gsap.set(bottles[2], { y: 120, opacity: 0, rotation: 8 });
gsap.set(leaves, { scale: 0, opacity: 0, transformOrigin: 'center center' });
gsap.set(scoop, { y: 60, x: -40, opacity: 0, rotation: -20 });
gsap.set(bowl, { y: 60, x: 40, opacity: 0, rotation: 20 });
gsap.set(partners, { y: 10, opacity: 0 });
// -------------------------------------------------------------------
// LOADER TIMELINE โ Bottle fills with powder
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Scoop tilts back and forth (pouring)
gsap.to(loaderScoop, {
rotation: 12,
duration: 0.8,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
transformOrigin: '30% 50%',
});
// Particles fall continuously
particleEls.forEach((el, i) => {
const delay = (i % 6) * 0.15;
gsap.fromTo(el,
{
y: 0,
x: () => gsap.utils.random(-18, 18),
opacity: 1,
scale: gsap.utils.random(0.6, 1.2),
},
{
y: 280,
opacity: 0,
duration: () => gsap.utils.random(1.2, 1.8),
repeat: -1,
delay,
ease: 'power1.in',
}
);
});
// Counter + bottle fill
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2.4,
ease: 'power1.inOut',
onUpdate: () => {
const v = p.v;
loaderCounter.textContent = Math.floor(v) + '%';
// Fill animates from bottom (y=326) growing upward
const fillHeight = (v / 100) * 260;
loaderFill.setAttribute('height', fillHeight);
loaderFill.setAttribute('y', 326 - fillHeight);
// Foam bubbles at liquid surface (only when filling)
if (Math.random() > 0.85 && v < 98) {
const bubble = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
const x = 40 + Math.random() * 120;
const y = 326 - fillHeight + 2;
bubble.setAttribute('cx', x);
bubble.setAttribute('cy', y);
bubble.setAttribute('r', 1.5 + Math.random() * 2);
bubble.setAttribute('fill', '#9ed67e');
bubble.setAttribute('opacity', '0.8');
loaderFoam.appendChild(bubble);
gsap.to(bubble, {
cy: y - 10,
opacity: 0,
duration: 0.6,
onComplete: () => bubble.remove(),
});
}
// Label changes at thresholds
if (v > 30 && loaderLabel.textContent === 'MIXING') loaderLabel.textContent = 'SHAKING';
if (v > 65 && loaderLabel.textContent === 'SHAKING') loaderLabel.textContent = 'ALMOST THERE';
if (v > 95 && loaderLabel.textContent === 'ALMOST THERE') loaderLabel.textContent = 'READY TO SERVE';
},
});
// Exit: bottle shakes like a shaker, then whole scene fades
loaderTl.to(loaderScoop, { opacity: 0, duration: 0.3, y: -40 }, '+=0.1');
loaderTl.to('.loader__bottle', {
rotation: -3,
duration: 0.08,
yoyo: true,
repeat: 5,
ease: 'sine.inOut',
transformOrigin: 'center bottom',
}, '<');
loaderTl.to([loaderCounter, loaderLabel], {
y: -10, opacity: 0, duration: 0.3, stagger: 0.05,
}, '-=0.3');
loaderTl.to('.loader__scene', {
scale: 1.1, opacity: 0, y: -30, duration: 0.6, ease: 'power3.in',
}, '-=0.2');
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' } });
// Nav
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.05,
}, 0);
// Eyebrow + fades (except partners)
tl.to(fades[0], { y: 0, opacity: 1, duration: 0.6 }, 0.3);
// Title chars
tl.to(titleChars, {
yPercent: 0, opacity: 1,
duration: 1,
stagger: 0.02,
ease: 'expo.out',
}, 0.5);
// CTA
tl.to(cta, {
y: 0, opacity: 1, scale: 1,
duration: 0.7,
ease: 'back.out(1.6)',
}, 0.9);
// Green arch scales up
tl.to(arch, {
scale: 1, opacity: 1,
duration: 1.2,
ease: 'power4.out',
}, 1);
// Bottles drop in (side ones first, center last with bounce)
tl.to(bottles[0], {
y: 0, opacity: 1,
duration: 1,
ease: 'power4.out',
}, 1.2);
tl.to(bottles[2], {
y: 0, opacity: 1,
duration: 1,
ease: 'power4.out',
}, 1.3);
tl.to(bottles[1], {
y: 0, opacity: 1,
duration: 1.2,
ease: 'back.out(1.4)',
}, 1.4);
// Leaves bloom in
tl.to(leaves, {
scale: 1, opacity: 1,
duration: 0.8,
stagger: 0.07,
ease: 'back.out(2)',
}, 1.3);
// Scoop + bowl
tl.to([scoop, bowl], {
y: 0, x: 0, opacity: 1, rotation: 0,
duration: 0.9,
stagger: 0.1,
ease: 'power4.out',
}, 1.5);
// Partners (from the outer fade group 1 onwards)
if (fades[1]) tl.to(fades[1], { y: 0, opacity: 1, duration: 0.6 }, 1.9);
tl.to(partners, {
y: 0, opacity: 0.6,
duration: 0.5,
stagger: 0.06,
}, 2);
tl.call(startContinuous, null, 2.2);
tl.call(enableInteractions, null, 2.2);
}
// -------------------------------------------------------------------
// CONTINUOUS ANIMATIONS
// -------------------------------------------------------------------
function startContinuous() {
// CTA glow pulse
gsap.to(ctaGlow, {
opacity: 0.7,
duration: 1.2,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Leaves sway individually
leaves.forEach((leaf, i) => {
gsap.to(leaf, {
rotation: `+=${i % 2 === 0 ? 6 : -6}`,
duration: 3 + i * 0.3,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
transformOrigin: 'bottom center',
});
});
// Bottles subtle idle bob (individual per bottle, factored into parallax)
bottles.forEach((b, i) => {
b.dataset.idleT = String(i * 0.7);
});
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic ----
magnetics.forEach((el) => {
const strength = el.classList.contains('hero__cta') ? 0.35
: el.classList.contains('topnav__menu') ? 0.4
: el.classList.contains('topnav__icon') ? 0.3
: 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)' });
});
});
// ---- Title char proximity + hover ----
const hero = document.getElementById('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) * 12 });
else gsap.set(c, { y: 0 });
});
});
titleChars.forEach((c) => {
c.addEventListener('mouseenter', () => gsap.to(c, { color: '#3a9f2a', duration: 0.2 }));
c.addEventListener('mouseleave', () => gsap.to(c, { color: '#0f0f10', duration: 0.4 }));
});
// ---- Cart counter scramble on hover ----
cartEl.addEventListener('mouseenter', () => {
const start = parseInt(cartCount.textContent, 10) || 0;
const target = start + 1 + Math.floor(Math.random() * 3);
const obj = { v: start };
gsap.to(obj, {
v: target,
duration: 0.4,
snap: 'v',
ease: 'power2.out',
onUpdate: () => cartCount.textContent = Math.round(obj.v),
});
gsap.fromTo(cartEl.querySelector('.topnav__badge'),
{ scale: 1 },
{ scale: 1.4, duration: 0.2, yoyo: true, repeat: 1, ease: 'back.out(2)' }
);
});
cartEl.addEventListener('mouseleave', () => {
setTimeout(() => {
const obj = { v: parseInt(cartCount.textContent, 10) || 0 };
gsap.to(obj, {
v: 3,
duration: 0.4,
snap: 'v',
ease: 'power2.in',
onUpdate: () => cartCount.textContent = Math.round(obj.v),
});
}, 300);
});
// ---- Bottle hover: lift + tilt ----
bottles.forEach((b, i) => {
b.addEventListener('mouseenter', () => {
const baseRot = bottleBaseRot[i];
const baseScale = bottleBaseScale[i];
gsap.to(b, {
y: -25,
rotation: baseRot * 0.3,
scale: baseScale * 1.08,
duration: 0.5,
ease: 'back.out(2)',
});
});
b.addEventListener('mouseleave', () => {
gsap.to(b, {
y: 0,
rotation: bottleBaseRot[i],
scale: bottleBaseScale[i],
duration: 0.7,
ease: 'elastic.out(1, 0.4)',
});
});
// Click: shake
b.addEventListener('click', (e) => {
e.preventDefault();
gsap.fromTo(b,
{ rotation: bottleBaseRot[i] - 6 },
{
rotation: bottleBaseRot[i] + 6,
duration: 0.08,
yoyo: true,
repeat: 5,
ease: 'sine.inOut',
onComplete: () => gsap.to(b, { rotation: bottleBaseRot[i], duration: 0.3 }),
}
);
});
});
// ---- Leaves: follow cursor slightly ----
leaves.forEach((leaf, i) => {
leaf.addEventListener('mouseenter', () => {
gsap.to(leaf, {
scale: 1.15,
duration: 0.4,
ease: 'back.out(2)',
});
});
leaf.addEventListener('mouseleave', () => {
gsap.to(leaf, {
scale: 1,
duration: 0.5,
ease: 'elastic.out(1, 0.5)',
});
});
});
// ---- Scoop + bowl hover ----
scoop.addEventListener('mouseenter', () => {
gsap.to(scoop, { rotation: -12, y: -6, duration: 0.4, ease: 'back.out(2)' });
});
scoop.addEventListener('mouseleave', () => {
gsap.to(scoop, { rotation: 0, y: 0, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
});
bowl.addEventListener('mouseenter', () => {
gsap.to(bowl, { rotation: 360, duration: 0.8, ease: 'power2.inOut' });
});
bowl.addEventListener('mouseleave', () => {
gsap.set(bowl, { rotation: 0 });
});
// ---- Partner logos: grayscale removed on hover (already in CSS). Add wiggle ----
partners.forEach((pt) => {
pt.addEventListener('mouseenter', () => {
gsap.fromTo(pt,
{ rotation: -2 },
{ rotation: 0, duration: 0.5, ease: 'elastic.out(1, 0.4)' }
);
});
});
// ---- Scene parallax ----
let mX = 0, mY = 0, cX = 0, cY = 0;
const bottlesScene = document.getElementById('bottles');
bottlesScene.addEventListener('mousemove', (e) => {
const r = bottlesScene.getBoundingClientRect();
mX = ((e.clientX - r.left) / r.width - 0.5) * 2;
mY = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
bottlesScene.addEventListener('mouseleave', () => { mX = 0; mY = 0; });
let idleStart = performance.now();
gsap.ticker.add(() => {
cX += (mX - cX) * 0.05;
cY += (mY - cY) * 0.05;
const t = (performance.now() - idleStart) * 0.001;
// Arch & bowl move least
gsap.set(arch, { x: cX * 8, y: cY * 4 });
gsap.set(scoop, { x: -cX * 10, y: -cY * 6 + Math.sin(t * 1.2) * 3, rotation: -cX * 1 });
gsap.set(bowl, { x: cX * 10, y: -cY * 6 + Math.sin(t * 1.1 + 1) * 3 });
// Bottles: parallax + idle bob, preserving base rotation/scale
bottles.forEach((b, i) => {
if (b.matches(':hover')) return;
const baseRot = bottleBaseRot[i];
const baseScale = bottleBaseScale[i];
const bob = Math.sin(t * 1.2 + i * 0.9) * 6;
gsap.set(b, {
x: cX * (14 + i * 2),
y: bob + cY * 6,
rotation: baseRot + cX * 1.4,
scale: baseScale,
});
});
// Leaves drift with cursor
leaves.forEach((leaf, i) => {
if (leaf.matches(':hover')) return;
const dir = i % 2 === 0 ? 1 : -1;
gsap.set(leaf, {
x: cX * (18 + i * 4) * dir,
y: cY * (12 + i * 2),
});
});
});
}
})();