import { TextSplitter } from './textSplitter.js';
// Initialize GSAP ScrollTrigger
gsap.registerPlugin(ScrollTrigger);
// Select all (fixed) elements on the page with a specific data attribute ('data-fixed').
const fixedElements = document.querySelectorAll('[data-fixed]');
const blocks = document.querySelectorAll('.content__inner'); // Content blocks that trigger the animations
// Default animation properties
const defaultAnimationProps = {
duration: 0.6,
ease: 'power4'
};
// Store original HTML content and styles for each fixed element for restoring later
const originalHTMLContent = new Map();
const originalStyles = new Map();
fixedElements.forEach(el => {
originalHTMLContent.set(el, el.innerHTML);
originalStyles.set(el, el.getAttribute('style') || ''); // Store initial inline styles if any
});
// Function to reset an element to its original state
function resetElement(target) {
// Restore original HTML content
target.innerHTML = originalHTMLContent.get(target);
// Remove any transformations and set properties back to default values
gsap.set(target, {
clearProps: 'all', // Clear all GSAP-applied properties
rotation: 0,
xPercent: 0,
yPercent: 0,
x: 0,
y: 0,
scale: 1,
autoAlpha: 1,
});
// Restore any initial inline styles if present
target.setAttribute('style', originalStyles.get(target));
}
// Function to get the fixed element's position from the top of the viewport
function getElementTopOffset(element) {
const elementRect = element.getBoundingClientRect();
return elementRect.top; // Returns the distance of the element from the top of the viewport
}
// Grouped animation effects, each with optional offsetStartAmount and offsetEndAmount (offset to add to the ScrollTrigger's start/end properties)
const effects = [
// Effect 1: Scale down and hide, then scale up and show
{
offsetStartAmount: 30,
offsetEndAmount: 10,
onEnter: (target) => {
if (target.currentTween) target.currentTween.kill();
resetElement(target); // Reset element state before animation starts
gsap.set(target, { transformOrigin: '0% 50%' });
target.currentTween = gsap.to(target, {
scale: 0,
autoAlpha: 0,
...defaultAnimationProps,
onComplete: () => {
target.currentTween = null;
}
});
},
onLeave: (target) => {
if (target.currentTween) target.currentTween.kill();
target.currentTween = gsap.to(target, {
scale: 1,
autoAlpha: 1,
...defaultAnimationProps,
onComplete: () => {
resetElement(target); // Reset element state
target.currentTween = null;
}
});
}
},
// Effect 2: Blur effect and scale change
{
offsetStartAmount: 30,
offsetEndAmount: 10,
onEnter: (target) => {
if (target.currentTween) target.currentTween.kill();
resetElement(target); // Reset element state before animation starts
gsap.set(target, { transformOrigin: '0% 50%' });
target.currentTween = gsap.to(target, {
startAt: { filter: 'blur(0px)' },
filter: 'blur(5px)',
scale: 0.9,
duration: 0.2,
ease: 'sine',
onComplete: () => {
target.currentTween = null;
}
});
},
onLeave: (target) => {
if (target.currentTween) target.currentTween.kill();
target.currentTween = gsap.to(target, {
filter: 'blur(0px)',
scale: 1,
duration: 0.6,
ease: 'expo',
onComplete: () => {
resetElement(target); // Reset element state
target.currentTween = null;
}
});
}
},
// Effect 3: Slide text up effect
{
offsetStartAmount: 30,
offsetEndAmount: 10,
onEnter: (target) => {
if (target.currentTween) target.currentTween.kill();
resetElement(target); // Reset element state before animation starts
const innerContent = target.innerHTML;
target.innerHTML = `
${innerContent}
`;
target.classList.add('oh');
gsap.set(target, { transformOrigin: '50% 50%' });
target.currentTween = gsap.to(target.querySelector('.oh__inner'), {
yPercent: -102,
...defaultAnimationProps,
onComplete: () => {
target.currentTween = null;
}
});
},
onLeave: (target) => {
if (target.currentTween) target.currentTween.kill();
target.currentTween = gsap.to(target.querySelector('.oh__inner'), {
startAt: { yPercent: 102 },
yPercent: 0,
...defaultAnimationProps,
onComplete: () => {
target.innerHTML = originalHTMLContent.get(target);
target.classList.remove('oh');
resetElement(target); // Reset element state
target.currentTween = null;
}
});
}
},
// Effect 4: using TextSplitter
{
offsetStartAmount: 30,
offsetEndAmount: 10,
onEnter: (target) => {
if (target.currentTween) target.currentTween.kill();
resetElement(target); // Reset element state before animation starts
target.textSplitter = new TextSplitter(target, { splitTypeTypes: 'chars' });
target.currentTween = gsap.to(target.textSplitter.getChars(), {
x: () => gsap.utils.random(-50, 50),
y: () => gsap.utils.random(-40, 0),
autoAlpha: 0,
stagger: {
amount: 0.03,
from: 'random'
},
...defaultAnimationProps,
onComplete: () => {
target.currentTween = null;
}
});
},
onLeave: (target) => {
if (target.currentTween) target.currentTween.kill();
target.currentTween = gsap.to(target.textSplitter.getChars(), {
x: 0,
y: 0,
autoAlpha: 1,
stagger: {
amount: 0.03,
from: 'random'
},
onComplete: () => {
target.innerHTML = originalHTMLContent.get(target);
resetElement(target); // Reset element state
target.currentTween = null;
},
...defaultAnimationProps
});
}
},
// Effect 5: Split characters and animate with stagger direction control and shuffle effect
{
offsetStartAmount: 30,
offsetEndAmount: 10,
onEnter: (target) => {
if (target.currentTween) target.currentTween.kill();
resetElement(target); // Reset element state before animation starts
target.textSplitter = new TextSplitter(target, { splitTypeTypes: 'chars' });
target.currentTween = gsap.to(target.textSplitter.getChars(), {
duration: 0.02,
ease: 'none',
autoAlpha: 0,
stagger: {
amount: 0.25,
from: 'end'
},
onComplete: () => {
target.currentTween = null;
}
});
},
onLeave: (target) => {
if (target.currentTween) target.currentTween.kill();
const chars = target.textSplitter.getChars();
const getRandomChar = () => {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
return letters.charAt(Math.floor(Math.random() * letters.length));
};
// Create a GSAP timeline to animate character shuffling and restore original content
const tl = gsap.timeline({
onComplete: () => {
resetElement(target); // Reset element state after entire animation completes
target.currentTween = null; // Clear the reference to the tween
}
});
// Iterate through each character and add shuffling effect to timeline
chars.forEach((char, index) => {
const originalChar = char.innerHTML;
// Perform 2 shuffling iterations
for (let i = 0; i < 2; i++) {
tl.to(char, {
duration: 0.03,
textContent: getRandomChar(),
autoAlpha: 1,
ease: 'none'
});
}
// Restore original character content
tl.to(char, {
duration: 0.02,
textContent: originalChar,
autoAlpha: 1,
ease: 'none'
});
// Delay the start of each character's timeline based on index
tl.add('', index * 0.03);
});
// Store reference to the timeline
target.currentTween = tl;
}
},
// Effect 6: Move and rotate simultaneously
{
offsetStartAmount: 90,
offsetEndAmount: 40,
onEnter: (target) => {
if (target.currentTween) target.currentTween.kill();
resetElement(target); // Reset element state before animation starts
gsap.set(target, { transformOrigin: '0% 100%' });
target.currentTween = gsap.to(target, {
xPercent: -5,
rotation: -90,
y: () => target.offsetWidth - target.offsetHeight,
duration: 0.6,
ease: 'power4',
onComplete: () => {
target.currentTween = null;
}
});
},
onLeave: (target) => {
if (target.currentTween) target.currentTween.kill();
target.currentTween = gsap.to(target, {
rotation: 0,
xPercent: 0,
y: 0,
...defaultAnimationProps,
onComplete: () => {
resetElement(target); // Reset element state
target.currentTween = null;
},
});
}
},
// Effect 7: move away
{
offsetStartAmount: 30,
offsetEndAmount: 10,
onEnter: (target) => {
if (target.currentTween) target.currentTween.kill();
resetElement(target); // Reset element state before animation starts
target.currentTween = gsap.to(target, {
x: () => -1* (target.offsetWidth + target.offsetLeft),
...defaultAnimationProps,
onComplete: () => {
target.currentTween = null; // Clear the reference to the tween
}
});
},
onLeave: (target) => {
if (target.currentTween) target.currentTween.kill();
target.currentTween = gsap.to(target, {
x: 0,
...defaultAnimationProps,
onComplete: () => {
resetElement(target); // Reset element state
target.currentTween = null; // Clear the reference to the tween
},
})
}
}
];
// Function to create ScrollTriggers for each content block and fixed element
function createScrollTriggers() {
blocks.forEach((block, index) => {
const effect = effects[index % effects.length]; // Select the effect for the current block
fixedElements.forEach((fixedElement) => {
let elementOffsetTop = getElementTopOffset(fixedElement); // Calculate offset for the fixed element
ScrollTrigger.create({
trigger: block, // Element to observe (content block)
start: () => `top ${elementOffsetTop + effect.offsetStartAmount}px`, // Use effect-specific offset
end: () => `bottom ${elementOffsetTop - effect.offsetEndAmount}px`, // Use effect-specific offset
onEnter: () => effect.onEnter(fixedElement),
onLeaveBack: () => effect.onLeaveBack ? effect.onLeaveBack(fixedElement) : effect.onLeave(fixedElement),
onLeave: () => effect.onLeave(fixedElement),
onEnterBack: () => effect.onEnterBack ? effect.onEnterBack(fixedElement) : effect.onEnter(fixedElement)
});
});
});
}
// Wait until the page is fully loaded to create the ScrollTriggers
window.addEventListener('load', () => {
createScrollTriggers();
});
// Update position dynamically on resize
window.addEventListener('resize', () => {
ScrollTrigger.refresh(); // Refresh ScrollTrigger to apply new start/end positions
});