(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(); } })();