(function () {
const gsapSrc = 'https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js';
const descriptorSelector = '[data-rotating-descriptor]';
const css = `
[data-rotating-descriptor] {
will-change: transform, opacity, filter, clip-path;
}
[data-rotating-descriptor][data-rotating-reserve-height="true"] {
min-height: 2.8em;
}
[data-rotating-descriptor] .rtd-word {
display: inline-block;
white-space: pre;
will-change: transform, opacity, filter;
}
@media (max-width: 767px) {
[data-rotating-disable-mobile="true"] {
display: none !important;
min-height: 0 !important;
transform: none !important;
}
}
`;
function injectCSS() {
if (document.getElementById('rotating-descriptor-css')) return;
const style = document.createElement('style');
style.id = 'rotating-descriptor-css';
style.textContent = css;
const target = document.head || document.documentElement;
target.appendChild(style);
}
function loadGSAP(callback) {
if (window.gsap) {
callback();
return;
}
const existingScript = document.querySelector('script[src="' + gsapSrc + '"]');
if (existingScript) {
existingScript.addEventListener('load', callback, { once: true });
return;
}
const script = document.createElement('script');
script.src = gsapSrc;
script.onload = callback;
script.onerror = function () {
console.warn('GSAP could not be loaded.');
};
document.head.appendChild(script);
}
function escapeHTML(value) {
return String(value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function getAttributeText(element, attributeName) {
const value = element.getAttribute(attributeName);
return value === null ? '' : value.trim();
}
function getBreakSymbol(element) {
const value = element.getAttribute('data-rotating-break');
if (value === null) return '|';
if (value.toLowerCase() === 'none') return '';
return value;
}
function splitTextByBreakSymbol(text, breakSymbol) {
if (!breakSymbol) return [String(text)];
return String(text).split(breakSymbol);
}
function textToHTML(text, breakSymbol) {
return splitTextByBreakSymbol(text, breakSymbol)
.map(function (line) {
return escapeHTML(line);
})
.join('
');
}
function textToWordHTML(text, breakSymbol) {
return splitTextByBreakSymbol(text, breakSymbol)
.map(function (line) {
return line
.split(/(\s+)/)
.map(function (token) {
if (!token) return '';
if (/^\s+$/.test(token)) {
return escapeHTML(token);
}
return '' + escapeHTML(token) + '';
})
.join('');
})
.join('
');
}
function renderPhrase(element, text, animationName, breakSymbol) {
if (animationName === 'words') {
element.innerHTML = textToWordHTML(text, breakSymbol);
return;
}
element.innerHTML = textToHTML(text, breakSymbol);
}
function getConfig(element) {
return {
animation: (element.getAttribute('data-rotating-animation') || 'slide-blur').trim().toLowerCase(),
holdTime: parseFloat(element.getAttribute('data-rotating-hold')) || 3.5,
outDuration: parseFloat(element.getAttribute('data-rotating-out-duration')) || 0.45,
inDuration: parseFloat(element.getAttribute('data-rotating-in-duration')) || 0.65,
yDistance: parseFloat(element.getAttribute('data-rotating-y')) || 12,
easeOut: element.getAttribute('data-rotating-ease-out') || '',
easeIn: element.getAttribute('data-rotating-ease-in') || '',
breakSymbol: getBreakSymbol(element)
};
}
function isElementVisibleForAnimation(element) {
if (!element || !element.isConnected) return false;
const computedStyle = getComputedStyle(element);
if (
computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden' ||
computedStyle.contentVisibility === 'hidden'
) {
return false;
}
const rects = element.getClientRects();
if (!rects.length) {
return false;
}
return true;
}
function clearGSAPInlineProps(element) {
if (!window.gsap) return;
gsap.killTweensOf(element);
gsap.set(element, {
clearProps: 'transform,opacity,visibility,filter,clipPath,scale,rotationX,transformPerspective,transformOrigin'
});
}
function animateStandard(element, nextText, config, onComplete) {
const animation = config.animation;
const timeline = gsap.timeline({ onComplete: onComplete });
if (animation === 'slide') {
timeline
.to(element, {
autoAlpha: 0,
y: -config.yDistance,
duration: config.outDuration,
ease: config.easeOut || 'power2.inOut'
})
.call(function () {
renderPhrase(element, nextText, animation, config.breakSymbol);
})
.set(element, {
y: config.yDistance
})
.to(element, {
autoAlpha: 1,
y: 0,
duration: config.inDuration,
ease: config.easeIn || 'power3.out'
});
return timeline;
}
if (animation === 'scale') {
timeline
.to(element, {
autoAlpha: 0,
y: -4,
scale: 0.96,
filter: 'blur(5px)',
duration: config.outDuration,
ease: config.easeOut || 'power2.inOut'
})
.call(function () {
renderPhrase(element, nextText, animation, config.breakSymbol);
})
.set(element, {
y: 8,
scale: 1.04,
filter: 'blur(6px)'
})
.to(element, {
autoAlpha: 1,
y: 0,
scale: 1,
filter: 'blur(0px)',
duration: config.inDuration,
ease: config.easeIn || 'back.out(1.4)'
});
return timeline;
}
if (animation === 'flip') {
timeline
.set(element, {
transformPerspective: 700,
transformOrigin: '50% 50%'
})
.to(element, {
autoAlpha: 0,
y: -6,
rotationX: -70,
filter: 'blur(4px)',
duration: config.outDuration,
ease: config.easeOut || 'power2.inOut'
})
.call(function () {
renderPhrase(element, nextText, animation, config.breakSymbol);
})
.set(element, {
y: 8,
rotationX: 70,
filter: 'blur(4px)'
})
.to(element, {
autoAlpha: 1,
y: 0,
rotationX: 0,
filter: 'blur(0px)',
duration: config.inDuration,
ease: config.easeIn || 'back.out(1.2)'
});
return timeline;
}
if (animation === 'clip') {
timeline
.set(element, {
clipPath: 'inset(0% 0% 0% 0%)'
})
.to(element, {
autoAlpha: 0,
y: -4,
clipPath: 'inset(0% 0% 100% 0%)',
duration: config.outDuration,
ease: config.easeOut || 'power2.inOut'
})
.call(function () {
renderPhrase(element, nextText, animation, config.breakSymbol);
})
.set(element, {
autoAlpha: 1,
y: 4,
clipPath: 'inset(100% 0% 0% 0%)'
})
.to(element, {
y: 0,
clipPath: 'inset(0% 0% 0% 0%)',
duration: config.inDuration,
ease: config.easeIn || 'power3.out'
});
return timeline;
}
timeline
.to(element, {
autoAlpha: 0,
y: -config.yDistance,
filter: 'blur(6px)',
duration: config.outDuration,
ease: config.easeOut || 'power2.inOut'
})
.call(function () {
renderPhrase(element, nextText, animation, config.breakSymbol);
})
.set(element, {
y: config.yDistance,
filter: 'blur(8px)'
})
.to(element, {
autoAlpha: 1,
y: 0,
filter: 'blur(0px)',
duration: config.inDuration,
ease: config.easeIn || 'power3.out'
});
return timeline;
}
function animateWords(element, nextText, config, onComplete) {
let oldWords = element.querySelectorAll('.rtd-word');
const timeline = gsap.timeline({ onComplete: onComplete });
if (!oldWords.length) {
renderPhrase(element, element.textContent.trim(), 'words', config.breakSymbol);
oldWords = element.querySelectorAll('.rtd-word');
}
timeline
.to(oldWords, {
autoAlpha: 0,
y: -10,
filter: 'blur(4px)',
duration: config.outDuration,
stagger: 0.025,
ease: config.easeOut || 'power2.inOut'
})
.call(function () {
renderPhrase(element, nextText, 'words', config.breakSymbol);
})
.fromTo(
element.querySelectorAll('.rtd-word'),
{
autoAlpha: 0,
y: 12,
filter: 'blur(5px)'
},
{
autoAlpha: 1,
y: 0,
filter: 'blur(0px)',
duration: config.inDuration,
stagger: 0.035,
ease: config.easeIn || 'power3.out'
}
);
return timeline;
}
function initRotatingDescriptorElement(element) {
if (element.dataset.rotatingDescriptorInitialized === 'true') return;
const config = getConfig(element);
const textOneFromAttribute = getAttributeText(element, 'data-rotating-text-1');
const textTwoFromAttribute = getAttributeText(element, 'data-rotating-text-2');
if (!textTwoFromAttribute) {
console.warn('Missing data-rotating-text-2 attribute on rotating descriptor element.', element);
return;
}
element.dataset.rotatingDescriptorInitialized = 'true';
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const textOne = textOneFromAttribute || element.textContent.trim();
const textTwo = textTwoFromAttribute;
const phrases = [textOne, textTwo];
let currentIndex = 0;
let delayedCall = null;
let activeTimeline = null;
let isRunning = false;
let resizeTimer = null;
function killCurrentAnimation() {
if (delayedCall) {
delayedCall.kill();
delayedCall = null;
}
if (activeTimeline) {
activeTimeline.kill();
activeTimeline = null;
}
clearGSAPInlineProps(element);
isRunning = false;
}
function scheduleNextRotation() {
if (!isElementVisibleForAnimation(element)) {
killCurrentAnimation();
return;
}
if (delayedCall) {
delayedCall.kill();
}
delayedCall = gsap.delayedCall(config.holdTime, rotateText);
}
function rotateText() {
delayedCall = null;
if (!isElementVisibleForAnimation(element)) {
killCurrentAnimation();
return;
}
const nextIndex = currentIndex === 0 ? 1 : 0;
const nextText = phrases[nextIndex];
function onComplete() {
currentIndex = nextIndex;
activeTimeline = null;
scheduleNextRotation();
}
if (config.animation === 'words') {
activeTimeline = animateWords(element, nextText, config, onComplete);
} else {
activeTimeline = animateStandard(element, nextText, config, onComplete);
}
}
function startIfVisible() {
if (prefersReducedMotion) {
renderPhrase(element, textOne, config.animation, config.breakSymbol);
return;
}
if (!isElementVisibleForAnimation(element)) {
killCurrentAnimation();
return;
}
if (isRunning) return;
isRunning = true;
renderPhrase(element, phrases[currentIndex], config.animation, config.breakSymbol);
gsap.set(element, {
autoAlpha: 1,
y: 0,
scale: 1,
rotationX: 0,
filter: 'blur(0px)',
clipPath: 'inset(0% 0% 0% 0%)'
});
scheduleNextRotation();
}
function refreshAnimationState() {
if (resizeTimer) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(function () {
if (isElementVisibleForAnimation(element)) {
startIfVisible();
} else {
killCurrentAnimation();
}
}, 120);
}
startIfVisible();
window.addEventListener('resize', refreshAnimationState);
window.addEventListener('orientationchange', refreshAnimationState);
document.addEventListener('visibilitychange', refreshAnimationState);
const observer = new MutationObserver(refreshAnimationState);
observer.observe(element, {
attributes: true,
attributeFilter: ['class', 'style']
});
if (element.parentElement) {
observer.observe(element.parentElement, {
attributes: true,
attributeFilter: ['class', 'style']
});
}
}
function initRotatingDescriptor() {
injectCSS();
document.querySelectorAll(descriptorSelector).forEach(function (element) {
initRotatingDescriptorElement(element);
});
}
function start() {
loadGSAP(initRotatingDescriptor);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})();