JSbanner-playful-illustrated.js— GSAP animation timeline
(() => {
const root = document.getElementById('playful');
if (!root) return;
// -------------------------------------------------------------------
// SLIDE DATA (dummy content)
// -------------------------------------------------------------------
const slides = [
{ title: ['PROGRAM', 'ONE'], tag: 'CATEGORY A' },
{ title: ['PROGRAM', 'TWO'], tag: 'CATEGORY B' },
{ title: ['PROGRAM', 'THREE'], tag: 'CATEGORY C' },
];
let currentIndex = 0;
// -------------------------------------------------------------------
// ELEMENT REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderBlocks = loader.querySelectorAll('.loader__block');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const loaderCover = document.getElementById('loader-cover');
const loaderMeta = loader.querySelector('.loader__meta');
const loaderBlocksContainer = loader.querySelector('.loader__blocks');
const triLeft = root.querySelector('[data-el="tri-left"]');
const triRight = root.querySelector('[data-el="tri-right"]');
const cloud = root.querySelector('[data-el="cloud"]');
const birds = root.querySelector('[data-el="birds"]');
const castle = root.querySelector('[data-el="castle"]');
const castleParts = castle.querySelectorAll('[data-castle-part]');
const flower = root.querySelector('[data-el="flower"]');
const blob = root.querySelector('[data-el="blob"]');
const blobEyes = blob.querySelectorAll('.scene__blob-eye');
const child = root.querySelector('[data-el="child"]');
const dots = root.querySelectorAll('[data-el="dots"] .scene__dot');
const navEls = root.querySelectorAll('[data-nav-el]');
const card = root.querySelector('[data-card]');
const cardInner = document.getElementById('card-inner');
const cardLines = card.querySelectorAll('.card__line');
const cardTag = card.querySelector('.card__tag');
const cardDots = card.querySelectorAll('.card__dot');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
// -------------------------------------------------------------------
// INITIAL HIDDEN STATES
// -------------------------------------------------------------------
gsap.set(triLeft, { xPercent: -100, opacity: 0 });
gsap.set(triRight, { xPercent: 100, opacity: 0 });
gsap.set(cloud, { x: -200, opacity: 0 });
gsap.set(birds, { y: -40, opacity: 0 });
gsap.set(castleParts, { y: -60, opacity: 0, scale: 0.8, transformOrigin: 'center bottom' });
gsap.set(flower, { scale: 0, opacity: 0, transformOrigin: 'center center' });
gsap.set(blob, { y: 200, opacity: 0, scale: 0.7, transformOrigin: 'center bottom' });
gsap.set(child, { y: -250, opacity: 0, rotation: -20 });
gsap.set(dots, { scale: 0, opacity: 0, transformOrigin: 'center center' });
gsap.set(navEls, { y: -30, opacity: 0 });
gsap.set(card, { y: 120, opacity: 0, scale: 0.8 });
gsap.set(cardLines, { y: 60 });
gsap.set(loaderBlocks, { y: -200, opacity: 0 });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Blocks drop in sequence (bounce ease)
loaderTl.to(loaderBlocks[0], {
y: 0, opacity: 1, duration: 0.7, ease: 'bounce.out',
}, 0);
loaderTl.to(loaderBlocks[1], {
y: 0, opacity: 1, duration: 0.7, ease: 'bounce.out',
}, 0.15);
loaderTl.to(loaderBlocks[2], {
y: 0, opacity: 1, duration: 0.7, ease: 'bounce.out',
}, 0.45);
loaderTl.to(loaderBlocks[3], {
y: 0, opacity: 1, duration: 0.7, ease: 'bounce.out',
}, 0.7);
// Progress counter 0 -> 100
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 1.7,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = Math.round(p.v) + '%';
},
}, 0.1);
// Small wobble after all blocks land
loaderTl.to(loaderBlocksContainer, {
rotation: -3, duration: 0.12, yoyo: true, repeat: 3, ease: 'sine.inOut',
}, '+=0.2');
// Loader exits: blocks scatter, orange cover wipes up
loaderTl.to(loaderBlocks, {
y: (i) => -400 - i * 60,
x: (i) => (i - 1.5) * 120,
rotation: (i) => (i - 1.5) * 120,
opacity: 0,
duration: 0.9,
ease: 'power2.in',
stagger: 0.04,
}, '+=0.2');
loaderTl.to(loaderMeta, {
opacity: 0, y: -20, duration: 0.4, ease: 'power2.in',
}, '<');
loaderTl.to(loaderCover, {
y: '-100%',
height: '100%',
duration: 1,
ease: 'power4.inOut',
}, '-=0.3');
loaderTl.to(loader, {
opacity: 0, duration: 0.3, ease: 'power2.in',
}, '+=0.1');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Background diagonals slide in from sides
tl.to(triLeft, { xPercent: 0, opacity: 1, duration: 0.9, ease: 'power4.out' }, 0);
tl.to(triRight, { xPercent: 0, opacity: 1, duration: 0.9, ease: 'power4.out' }, 0.05);
// Cloud drifts in
tl.to(cloud, { x: 0, opacity: 1, duration: 1.2, ease: 'power2.out' }, 0.2);
// Castle parts stack in one by one
tl.to(castleParts, {
y: 0, opacity: 1, scale: 1,
duration: 0.7,
stagger: 0.12,
ease: 'back.out(2)',
}, 0.35);
// Flower blooms
tl.to(flower, {
scale: 1, opacity: 1,
duration: 1,
ease: 'elastic.out(1, 0.55)',
}, 0.6);
// Blue blob pops up from bottom
tl.to(blob, {
y: 0, opacity: 1, scale: 1,
duration: 1,
ease: 'back.out(1.8)',
}, 0.8);
// Child drops down and settles
tl.to(child, {
y: 0, opacity: 1, rotation: 0,
duration: 1.1,
ease: 'bounce.out',
}, 1);
// Birds flutter in
tl.to(birds, { y: 0, opacity: 1, duration: 0.7 }, 1.25);
// Scatter dots
tl.to(dots, {
scale: 1, opacity: 1,
duration: 0.5,
stagger: 0.08,
ease: 'back.out(2)',
}, 1.35);
// Top nav
tl.to(navEls, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.08,
}, 0.25);
// Center card pops up
tl.to(card, {
y: 0, opacity: 1, scale: 1,
duration: 0.9,
ease: 'back.out(1.7)',
}, 1.3);
// Card title lines slide up
tl.to(cardLines, {
y: 0,
duration: 0.7,
stagger: 0.08,
ease: 'power3.out',
}, 1.55);
// Idle animations once main entrance finishes
tl.call(startIdleAnimations, null, 1.9);
}
// -------------------------------------------------------------------
// IDLE / CONTINUOUS ANIMATIONS (breathing, floating, blinking)
// -------------------------------------------------------------------
function startIdleAnimations() {
// Blob breathing
gsap.to(blob, {
y: '-=10',
duration: 2,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
});
// Child bobbing on swim ring
gsap.to(child, {
y: '-=12',
rotation: 3,
duration: 2.4,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
});
// Cloud slow drift
gsap.to(cloud, {
x: '+=30',
duration: 5,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
});
// Flower sway
gsap.to(flower, {
rotation: 6,
duration: 3,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
transformOrigin: 'center bottom',
});
// Birds subtle bob
gsap.to(birds, {
y: '-=8',
duration: 1.4,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
});
// Blob blink every few seconds
gsap.to(blobEyes, {
scaleY: 0.1,
duration: 0.12,
yoyo: true,
repeat: -1,
repeatDelay: 3.5,
ease: 'power1.inOut',
transformOrigin: 'center center',
});
}
// -------------------------------------------------------------------
// CAROUSEL (card content transitions)
// -------------------------------------------------------------------
function goToSlide(newIndex) {
newIndex = (newIndex + slides.length) % slides.length;
if (newIndex === currentIndex) return;
const direction = newIndex > currentIndex ? 1 : -1;
// Handle wrap-around direction
if (currentIndex === slides.length - 1 && newIndex === 0) direction;
currentIndex = newIndex;
const slide = slides[currentIndex];
const outTl = gsap.timeline({
onComplete: () => {
// Update content
cardLines[0].textContent = slide.title[0];
cardLines[1].textContent = slide.title[1];
cardTag.textContent = slide.tag;
cardDots.forEach((d, i) => d.classList.toggle('is-active', i === currentIndex));
// Animate in
gsap.set(cardLines, { y: 60 });
gsap.set(cardTag, { y: 30, opacity: 0 });
gsap.to(cardLines, {
y: 0,
duration: 0.6,
stagger: 0.07,
ease: 'power3.out',
});
gsap.to(cardTag, {
y: 0, opacity: 1,
duration: 0.5,
delay: 0.15,
ease: 'power3.out',
});
}
});
outTl.to(cardLines, {
y: -60,
duration: 0.35,
stagger: 0.04,
ease: 'power2.in',
}, 0);
outTl.to(cardTag, {
y: -20, opacity: 0,
duration: 0.3,
ease: 'power2.in',
}, 0);
// Playful card bounce
gsap.fromTo(card,
{ scale: 1 },
{ scale: 1.04, duration: 0.2, yoyo: true, repeat: 1, ease: 'sine.inOut' }
);
}
prevBtn.addEventListener('click', () => goToSlide(currentIndex - 1));
nextBtn.addEventListener('click', () => goToSlide(currentIndex + 1));
// Keyboard navigation
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') goToSlide(currentIndex - 1);
if (e.key === 'ArrowRight') goToSlide(currentIndex + 1);
});
// -------------------------------------------------------------------
// MOUSE PARALLAX (gentle)
// -------------------------------------------------------------------
let targetX = 0, targetY = 0;
let currentX = 0, currentY = 0;
let parallaxReady = false;
root.addEventListener('mousemove', (e) => {
const r = root.getBoundingClientRect();
targetX = ((e.clientX - r.left) / r.width - 0.5) * 2;
targetY = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
root.addEventListener('mouseleave', () => { targetX = 0; targetY = 0; });
gsap.delayedCall(4, () => { parallaxReady = true; });
gsap.ticker.add(() => {
if (!parallaxReady) return;
currentX += (targetX - currentX) * 0.05;
currentY += (targetY - currentY) * 0.05;
gsap.set(cloud, { x: currentX * 20, y: currentY * 10 });
gsap.set(birds, { x: currentX * 30 });
gsap.set(castle, { x: currentX * 12 });
gsap.set(flower, { x: currentX * 18 });
gsap.set(dots[0], { x: currentX * 30, y: currentY * 20 });
gsap.set(dots[1], { x: currentX * 25, y: currentY * 18 });
gsap.set(dots[2], { x: currentX * 35, y: currentY * 25 });
});
// Blob and child keep their idle loops — parallax would fight them.
})();