JSbanner-talent-playful.jsโ GSAP animation timeline
(() => {
const root = document.getElementById('playful');
if (!root) return;
// -------------------------------------------------------------------
// BUILD LOADER SPARKLES
// -------------------------------------------------------------------
const sparklesHost = document.getElementById('sparkles');
const SPARKLE_EMOJIS = ['\u2728', '\u2B50', '\u{1F4AB}'];
const SPARKLE_COUNT = 12;
for (let i = 0; i < SPARKLE_COUNT; i++) {
const s = document.createElement('span');
s.textContent = SPARKLE_EMOJIS[i % SPARKLE_EMOJIS.length];
const angle = (i / SPARKLE_COUNT) * Math.PI * 2;
const r = 120;
s.style.left = (160 + Math.cos(angle) * r * 0.6) + 'px';
s.style.top = (100 + Math.sin(angle) * r * 0.4) + 'px';
sparklesHost.appendChild(s);
}
const sparkleEls = sparklesHost.querySelectorAll('span');
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const rbPaths = loader.querySelectorAll('.rb');
const loaderUnicorn = loader.querySelector('.loader__unicorn');
const magnetics = root.querySelectorAll('[data-magnetic]');
const fades = root.querySelectorAll('[data-fade]');
const words = root.querySelectorAll('[data-word]');
const badge = root.querySelector('[data-badge]');
const rainbow = root.querySelector('[data-float]');
const people = root.querySelectorAll('[data-person]');
const emojis = root.querySelectorAll('[data-emoji]');
const unicornStage = root.querySelector('[data-unicorn]');
const unicornMain = root.querySelector('.unicorn-main');
const unicornPlatform = root.querySelector('.unicorn-platform');
const logos = root.querySelectorAll('[data-logo]');
const scrollDot = root.querySelector('[data-scroll]');
const logoSmile = document.getElementById('logo-smile');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -15, opacity: 0 });
gsap.set(rainbow, { scale: 0, opacity: 0, y: 20 });
gsap.set(badge, { scale: 0, opacity: 0 });
gsap.set(words, { yPercent: 120, opacity: 0 });
gsap.set(fades, { y: 20, opacity: 0 });
gsap.set(people, { scale: 0, opacity: 0 });
gsap.set(emojis, { scale: 0, opacity: 0, rotation: -20 });
gsap.set(unicornStage, { scale: 0, opacity: 0, rotation: -15 });
gsap.set(logos, { y: 15, opacity: 0 });
gsap.set(scrollDot, { opacity: 0 });
gsap.set(sparkleEls, { scale: 0, opacity: 0 });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Rainbow arcs draw in from inside-out
rbPaths.forEach((p, i) => {
loaderTl.to(p, {
strokeDashoffset: 0,
duration: 0.7,
ease: 'power2.out',
}, i * 0.06);
});
// Unicorn bounces in
loaderTl.from(loaderUnicorn, {
y: 40,
opacity: 0,
scale: 0.5,
duration: 0.7,
ease: 'back.out(2)',
}, 0.3);
// Unicorn bobbing idle
gsap.to(loaderUnicorn, {
y: -8,
duration: 0.7,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Sparkles pop + twinkle loop
sparkleEls.forEach((s, i) => {
loaderTl.to(s, {
scale: 1, opacity: 1,
duration: 0.3,
ease: 'back.out(2)',
}, 0.5 + i * 0.04);
gsap.to(s, {
scale: 1.4,
rotation: 20,
duration: 0.6 + Math.random() * 0.6,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: 0.5 + i * 0.08,
transformOrigin: 'center',
});
});
// Counter + labels
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2.2,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = Math.floor(p.v) + '%';
if (p.v > 30 && loaderLabel.textContent === 'GATHERING MAGIC') loaderLabel.textContent = 'SUMMONING UNICORNS';
if (p.v > 65 && loaderLabel.textContent === 'SUMMONING UNICORNS') loaderLabel.textContent = 'ALMOST THERE';
if (p.v > 95 && loaderLabel.textContent === 'ALMOST THERE') loaderLabel.textContent = 'MAGIC READY';
},
}, 0.1);
// Exit: sparkles burst outward, rainbow fades up
loaderTl.to(sparkleEls, {
x: (i) => (i - SPARKLE_COUNT / 2) * 30 + Math.random() * 40,
y: () => -80 - Math.random() * 60,
opacity: 0,
rotation: 180,
duration: 0.8,
ease: 'power3.in',
stagger: 0.02,
}, '+=0.3');
loaderTl.to([loaderUnicorn, loader.querySelector('.loader__rainbow')], {
y: -50, scale: 1.1, opacity: 0, duration: 0.5, ease: 'power3.in',
}, '-=0.7');
loaderTl.to([loaderCounter, loaderLabel], {
y: -10, opacity: 0, duration: 0.3, stagger: 0.05,
}, '-=0.6');
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' } });
// Nav
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.07,
}, 0);
// Rainbow + badge pop
tl.to(rainbow, {
scale: 1, opacity: 1, y: 0,
duration: 0.7,
ease: 'back.out(1.8)',
}, 0.3);
tl.to(badge, {
scale: 1, opacity: 1,
duration: 0.7,
ease: 'back.out(1.8)',
}, 0.45);
// Unicorn stage with bounce
tl.to(unicornStage, {
scale: 1, opacity: 1, rotation: 0,
duration: 1.1,
ease: 'back.out(1.5)',
}, 0.55);
// Title words: big bouncy reveal
tl.to(words, {
yPercent: 0, opacity: 1,
duration: 0.9,
stagger: { each: 0.1, from: 'start' },
ease: 'back.out(1.4)',
}, 0.75);
// Person bubbles bounce in (spring)
tl.to(people, {
scale: 1, opacity: 1,
duration: 0.8,
stagger: 0.1,
ease: 'back.out(2)',
}, 1);
// Emojis pop in with rotation
tl.to(emojis, {
scale: 1, opacity: 1, rotation: 0,
duration: 0.7,
stagger: 0.08,
ease: 'back.out(2)',
}, 1.15);
// Description + CTA + trusted
tl.to(fades, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.12,
}, 1.4);
// Trusted logos
tl.to(logos, {
y: 0, opacity: 0.5,
duration: 0.5,
stagger: 0.08,
}, 1.7);
// Scroll dot
tl.to(scrollDot, { opacity: 1, duration: 0.5 }, 1.9);
tl.call(startContinuous, null, 1.9);
tl.call(enableInteractions, null, 1.9);
}
// -------------------------------------------------------------------
// CONTINUOUS ANIMATIONS
// -------------------------------------------------------------------
function startContinuous() {
// Main unicorn idle bounce + rotation
gsap.to(unicornMain, {
y: -15,
rotation: 3,
duration: 1.6,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Platform gentle tilt
gsap.to(unicornPlatform, {
rotation: -8,
duration: 2.8,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Rainbow sway
gsap.to(rainbow, {
rotation: 8,
duration: 2,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
transformOrigin: 'center bottom',
});
// People bubbles idle float
people.forEach((person, i) => {
gsap.to(person, {
y: `-=${10 + i * 2}`,
duration: 2 + i * 0.3,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.2,
});
});
// Emojis idle float (random)
emojis.forEach((e, i) => {
gsap.to(e, {
y: '-=12',
rotation: (i % 2 === 0 ? 10 : -10),
duration: 2 + Math.random() * 1.5,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.15,
});
});
// Scroll dot pulse
gsap.to(scrollDot.querySelector('span'), {
scale: 1.5,
duration: 1,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Badge subtle shimmer
gsap.to(badge, {
y: -4,
duration: 2.5,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic ----
magnetics.forEach((el) => {
const strength = el.classList.contains('cta-big') ? 0.3
: el.classList.contains('topnav__cta') ? 0.3
: 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 smile stretches on hover ----
const logo = root.querySelector('.topnav__logo');
logo.addEventListener('mouseenter', () => {
gsap.fromTo(logoSmile,
{ scaleY: 1 },
{ scaleY: 1.6, duration: 0.3, yoyo: true, repeat: 1, ease: 'back.out(2)', transformOrigin: 'center top' }
);
});
// ---- Title words: hover color shift + wobble ----
words.forEach((w, i) => {
const colors = ['#6c55ff', '#ff5ed7', '#3b9ff5', '#ffd93b'];
w.addEventListener('mouseenter', () => {
gsap.to(w, {
color: colors[i % colors.length],
rotation: i % 2 === 0 ? -3 : 3,
scale: 1.05,
duration: 0.3,
ease: 'back.out(2)',
});
});
w.addEventListener('mouseleave', () => {
gsap.to(w, {
color: '#0a0a1a',
rotation: 0,
scale: 1,
duration: 0.5,
ease: 'elastic.out(1, 0.4)',
});
});
});
// ---- Unicorn click: spin + launch sparkles ----
unicornStage.addEventListener('click', () => {
gsap.to(unicornMain, {
rotation: '+=360',
duration: 1,
ease: 'power2.inOut',
});
// Spawn burst sparkles
spawnBurst(unicornStage);
});
unicornStage.addEventListener('mouseenter', () => {
gsap.to(unicornMain, { scale: 1.1, duration: 0.4, ease: 'back.out(2)' });
gsap.to(unicornPlatform, { scale: 1.05, duration: 0.4 });
});
unicornStage.addEventListener('mouseleave', () => {
gsap.to(unicornMain, { scale: 1, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
gsap.to(unicornPlatform, { scale: 1, duration: 0.5 });
});
// ---- Person bubbles: hover pop ----
people.forEach((person) => {
person.addEventListener('mouseenter', () => {
gsap.to(person, {
scale: 1.15,
rotation: gsap.utils.random(-8, 8),
duration: 0.4,
ease: 'back.out(2)',
});
});
person.addEventListener('mouseleave', () => {
gsap.to(person, {
scale: 1,
rotation: 0,
duration: 0.5,
ease: 'elastic.out(1, 0.4)',
});
});
});
// ---- Emojis: hover grow + wiggle ----
emojis.forEach((e) => {
e.addEventListener('mouseenter', () => {
gsap.to(e, {
scale: 1.4,
rotation: gsap.utils.random(-20, 20),
duration: 0.3,
ease: 'back.out(2.5)',
});
});
e.addEventListener('mouseleave', () => {
gsap.to(e, {
scale: 1,
rotation: 0,
duration: 0.5,
ease: 'elastic.out(1, 0.4)',
});
});
// Click = bounce away and back
e.addEventListener('click', () => {
gsap.fromTo(e,
{ y: 0 },
{
y: -40,
duration: 0.3,
yoyo: true,
repeat: 1,
ease: 'power2.out',
}
);
});
});
// ---- Badge: hover pops and rainbow appears ----
badge.addEventListener('mouseenter', () => {
gsap.to(badge, { scale: 1.06, duration: 0.3, ease: 'back.out(2)' });
gsap.to(rainbow, { scale: 1.2, y: -5, duration: 0.4, ease: 'back.out(2)' });
});
badge.addEventListener('mouseleave', () => {
gsap.to(badge, { scale: 1, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
gsap.to(rainbow, { scale: 1, y: 0, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
});
// ---- Trusted logos: grayscale reset + wobble ----
logos.forEach((lg) => {
lg.addEventListener('mouseenter', () => {
gsap.fromTo(lg,
{ rotation: -2 },
{ rotation: 0, duration: 0.5, ease: 'elastic.out(1, 0.4)' }
);
});
});
// ---- Mouse parallax for unicorn stage + emojis ----
let mx = 0, my = 0, cmx = 0, cmy = 0;
const hero = document.getElementById('hero');
hero.addEventListener('mousemove', (e) => {
const r = hero.getBoundingClientRect();
mx = ((e.clientX - r.left) / r.width - 0.5) * 2;
my = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
hero.addEventListener('mouseleave', () => { mx = 0; my = 0; });
gsap.ticker.add(() => {
cmx += (mx - cmx) * 0.05;
cmy += (my - cmy) * 0.05;
// Don't override hover on unicorn
if (!unicornStage.matches(':hover')) {
gsap.set(unicornStage, { x: cmx * 15, y: cmy * 8 });
}
emojis.forEach((e, i) => {
if (e.matches(':hover')) return;
gsap.set(e, { x: cmx * (10 + i * 4), y: cmy * (6 + i * 2) });
});
people.forEach((p, i) => {
if (p.matches(':hover')) return;
gsap.set(p, { x: cmx * (14 + i * 3), y: cmy * (10 + i * 2) });
});
});
}
// Spawn a sparkle burst at an element's center
function spawnBurst(target) {
const rect = target.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const burst = ['\u2728', '\u2B50', '\u{1F4AB}'];
for (let i = 0; i < 12; i++) {
const s = document.createElement('span');
s.textContent = burst[i % burst.length];
s.style.position = 'fixed';
s.style.left = cx + 'px';
s.style.top = cy + 'px';
s.style.fontSize = '22px';
s.style.pointerEvents = 'none';
s.style.zIndex = '200';
s.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(s);
const angle = (i / 12) * Math.PI * 2;
const dist = 100 + Math.random() * 80;
gsap.fromTo(s,
{ x: 0, y: 0, opacity: 1, scale: 0.5 },
{
x: Math.cos(angle) * dist,
y: Math.sin(angle) * dist,
opacity: 0,
scale: 1.4,
rotation: 180,
duration: 0.9,
ease: 'power3.out',
onComplete: () => s.remove(),
}
);
}
}
})();