UNPKG

textify-gsap

Version:

Advanced GSAP SplitText Animation Plugin with 20+ stunning text animation styles

597 lines (563 loc) 20.1 kB
/************************************************************************* * Textify v4.0.0 Enhanced Universal Text Animation Library * * https://github.com/mkk360/textify * * Copyright © 2025 mkk360 MIT License * * * * Enhanced with 15 new professional animations and performance * * optimizations for 2025 * ************************************************************************/ // --------------------------------------------------- // 1. Dependencies & Plugin Registration // --------------------------------------------------- (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Textify = factory()); }(this, (function () { 'use strict'; // GSAP core check with enhanced error handling if (typeof gsap === 'undefined') { throw new Error('Textify requires GSAP. Please include GSAP before Textify.'); } if (typeof SplitText === 'undefined') { throw new Error('Textify requires GSAP SplitText plugin.'); } // Register GSAP plugins with performance optimizations gsap.registerPlugin(SplitText, ScrollTrigger); gsap.config({ nullTargetWarn: false, force3D: true, autoSleep: 60 }); // --------------------------------------------------- // 2. Default Configuration // --------------------------------------------------- // Enhanced configuration with performance settings const defaultConfig = { selector: '[class*="textify-style"]', scrollTrigger: { start: 'top 80%', toggleActions: 'play none none reverse', once: false, refreshPriority: -1 } }; // --------------------------------------------------- // 3. Utility Functions // --------------------------------------------------- /** * Parse HTML data-* attributes into JS properties. * Converts kebab-case to camelCase and numeric strings to numbers. */ function parseDataAttrs(el) { const attrs = {}; Array.from(el.attributes).forEach(({ name, value }) => { if (name.startsWith('data-')) { const prop = name .slice(5) .replace(/-([a-z])/g, (_, c) => c.toUpperCase()); attrs[prop] = isNaN(value) ? value : +value; } }); return attrs; } /** * Inject minimal base CSS for character spans. */ function injectBaseCSS() { const css = ` [class*="textify-style"] .split-char { display: inline-block; transform-origin: center; will-change: transform, opacity; } [class*="textify-style"] { display: block; } [class*="textify-style"] .split-line, [class*="textify-style"] .split-char { display: inline-block; } /* Performance optimizations */ [class*="textify-style"] { -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1000; perspective: 1000; } /* Reduced motion support */ @media (prefers-reduced-motion: reduce) { [class*="textify-style"] .split-char { animation-duration: 0.01ms !important; animation-delay: 0ms !important; transition-duration: 0.01ms !important; transition-delay: 0ms !important; } } `; const styleEl = document.createElement('style'); styleEl.textContent = css; document.head.appendChild(styleEl); } // --------------------------------------------------- // 4. Animation Style Definitions // --------------------------------------------------- const animationConfigs = { style1: split => ({ styleClass: 'particle-explosion', x: () => gsap.utils.random(-800, 800), y: () => gsap.utils.random(-800, 800), rotation: () => gsap.utils.random(-360, 360), scale: () => gsap.utils.random(0.2, 2), opacity: 0, ease: 'power3.out', stagger: { amount: 1.5, from: 'random' }, duration: 1.8 }), style2: split => ({ styleClass: 'explosive-zoom', scale: 0, rotation: () => gsap.utils.random(-360, 360), opacity: 0, transformOrigin: 'center center', ease: 'elastic.out(1, 0.3)', stagger: 0.08, duration: 1.8 }), style3: split => ({ styleClass: 'wave-up', y: 100, skewY: 15, opacity: 0, ease: 'power2.out', stagger: 0.04, duration: 1.2 }), style4: split => ({ styleClass: 'flip-3d', rotationY: 180, rotationX: 90, opacity: 0, transformOrigin: 'center', ease: 'back.out(1.7)', stagger: 0.06, duration: 1.4 }), style5: split => ({ styleClass: 'bounce-scale', scale: 0.3, y: -80, opacity: 0, ease: 'bounce.out', stagger: 0.05, duration: 1.6 }), style6: split => ({ styleClass: 'spiral-zoom', scale: 0.1, rotation: 720, opacity: 0, transformOrigin: 'center', ease: 'power4.out', stagger: 0.05, duration: 1.3 }), style7: split => ({ styleClass: 'stretch-left', x: -200, scaleX: 0.1, opacity: 0, ease: 'elastic.out(1, 0.6)', stagger: 0.07, duration: 1.5 }), style8: split => ({ styleClass: 'float-particles', x: () => gsap.utils.random(-400, 400), y: () => gsap.utils.random(-800, -200), scale: () => gsap.utils.random(0.5, 1.5), opacity: 0, ease: 'power2.out', stagger: 0.02, duration: 2.0 }), style9: split => ({ styleClass: 'typewriter', scale: 0.5, y: 20, opacity: 0, ease: 'power2.out', stagger: 0.1, duration: 0.8 }), style10: split => ({ styleClass: 'magnetic-pull', x: () => gsap.utils.random(-600, 600), y: () => gsap.utils.random(-300, 300), scale: 0.7, rotation: () => gsap.utils.random(-90, 90), opacity: 0, ease: 'power3.out', stagger: 0.04, duration: 1.2 }), style11: split => ({ styleClass: 'flare-burst', scale: 0.2, opacity: 0, filter: 'brightness(200%)', ease: 'expo.out', stagger: { amount: 1, from: 'center' }, duration: 1.4 }), style12: split => ({ styleClass: 'ripple-wave', y: () => gsap.utils.random(50, 150), scaleX: 0.5, opacity: 0, ease: 'power2.inOut', stagger: { amount: 1.2, from: 'edges' }, duration: 1.6 }), style13: split => ({ styleClass: 'matrix-fall', y: -500, opacity: 0, skewY: 20, filter: 'grayscale(100%)', ease: 'power3.out', stagger: { amount: 1.5, from: 'random' }, duration: 2.0 }), style14: split => ({ styleClass: 'flip-carousel', rotationX: () => gsap.utils.random(90, 360), scale: 0.4, opacity: 0, transformOrigin: 'center', ease: 'back.out(1.5)', stagger: 0.05, duration: 1.8 }), style15: split => ({ styleClass: 'pulse-glow', scale: () => gsap.utils.random(0.8, 1.2), opacity: 0, filter: 'drop-shadow(0 0 10px cyan)', ease: 'sine.inOut', stagger: { amount: 1, from: 'center' }, duration: 1.2 }), style16: split => ({ styleClass: 'stagger-zoom', scale: 0, opacity: 0, ease: 'power4.in', stagger: { each: 0.1, from: 'end' }, duration: 1.0 }), style17: split => ({ styleClass: 'wave-fold', x: () => gsap.utils.random(-100, 100), y: () => gsap.utils.random(-50, 50), skewX: () => gsap.utils.random(-30, 30), opacity: 0, ease: 'elastic.out(1, 0.4)', stagger: { amount: 1.4, from: 'start' }, duration: 1.7 }), style18: split => ({ styleClass: 'sine-spray', x: () => gsap.utils.random(-300, 300), y: () => Math.sin(Date.now() % 360) * 200, scale: () => gsap.utils.random(0.5, 1.5), opacity: 0, ease: 'sine.out', stagger: 0.03, duration: 2.2 }), style19: split => { const cols = 10; const rows = Math.ceil(split.chars.length / cols); return { styleClass: 'flip-3d-grid', rotationY: () => gsap.utils.random(0, 360), rotationX: () => gsap.utils.random(0, 360), z: () => gsap.utils.random(-500, 500), opacity: 0, ease: 'back.out(2)', stagger: { grid: [rows, cols], from: 'center', amount: 2 }, duration: 2.5 }; }, style20: split => ({ styleClass: 'glow-trail', x: () => gsap.utils.random(-200, 200), y: () => gsap.utils.random(-200, 200), opacity: 0, filter: 'blur(4px) drop-shadow(0 0 20px magenta)', ease: 'power2.out', stagger: { amount: 1.5, from: 'edges' }, duration: 2.0 }), style21: split => ({ styleClass: 'glow-trail-small', x: 0, y: 0, opacity: 0, filter: 'blur(4px) drop-shadow(0 0 20px magenta)', ease: 'power2.out', stagger: { amount: 1.5, from: 'edges' }, duration: 2.0 }), // NEW PROFESSIONAL ANIMATIONS (22-36) - Modern 2025 Effects style22: split => ({ styleClass: 'morphing-text', scaleY: 0.1, skewX: 45, opacity: 0, transformOrigin: 'center bottom', ease: 'power4.out', stagger: 0.04, duration: 1.3 }), style23: split => ({ styleClass: 'neon-flicker', opacity: 0, filter: 'drop-shadow(0 0 5px #00ff88) brightness(150%)', ease: 'rough({ template: none.out, strength: 2, points: 20, taper: none, randomize: true, clamp: false })', stagger: 0.08, duration: 1.5 }), style24: split => ({ styleClass: 'liquid-wave', y: 60, scaleY: 0.3, skewY: () => gsap.utils.random(-20, 20), opacity: 0, ease: 'elastic.out(1, 0.8)', stagger: { amount: 1.2, from: 'center' }, duration: 1.8 }), style25: split => ({ styleClass: 'holographic-shift', x: () => gsap.utils.random(-30, 30), rotationY: () => gsap.utils.random(-45, 45), opacity: 0, filter: 'hue-rotate(180deg) saturate(150%)', ease: 'power2.out', stagger: 0.05, duration: 1.4 }), style26: split => ({ styleClass: 'glitch-matrix', x: () => gsap.utils.random(-20, 20), y: () => gsap.utils.random(-10, 10), opacity: 0, filter: 'contrast(150%) brightness(120%)', ease: 'rough({ template: none.out, strength: 1, points: 10, taper: none, randomize: true })', stagger: 0.02, duration: 1.1 }), style27: split => ({ styleClass: 'cinematic-reveal', y: 100, opacity: 0, scaleY: 0, transformOrigin: 'center top', ease: 'power4.out', stagger: { amount: 1.5, from: 'start' }, duration: 2.0 }), style28: split => ({ styleClass: 'floating-letters', y: () => gsap.utils.random(-30, 30), x: () => gsap.utils.random(-20, 20), rotation: () => gsap.utils.random(-15, 15), opacity: 0, ease: 'power2.out', stagger: 0.06, duration: 1.6 }), style29: split => ({ styleClass: 'digital-scan', scaleX: 0, opacity: 0, filter: 'brightness(200%) contrast(150%)', transformOrigin: 'left center', ease: 'power3.out', stagger: 0.03, duration: 1.2 }), style30: split => ({ styleClass: 'particle-storm', x: () => gsap.utils.random(-500, 500), y: () => gsap.utils.random(-300, 300), rotation: () => gsap.utils.random(-180, 180), scale: () => gsap.utils.random(0.3, 1.5), opacity: 0, ease: 'power3.out', stagger: { amount: 2, from: 'random' }, duration: 2.2 }), style31: split => ({ styleClass: 'vintage-fade', opacity: 0, filter: 'sepia(100%) contrast(120%) brightness(90%)', scale: 0.8, ease: 'power2.out', stagger: 0.1, duration: 1.8 }), style32: split => ({ styleClass: 'cyber-grid', y: -100, opacity: 0, skewY: -10, filter: 'drop-shadow(0 0 8px cyan) contrast(150%)', ease: 'power4.out', stagger: { amount: 1.3, from: 'edges' }, duration: 1.6 }), style33: split => ({ styleClass: 'elastic-bounce', scaleY: 0.1, y: 50, opacity: 0, transformOrigin: 'center bottom', ease: 'elastic.out(1.2, 0.4)', stagger: 0.05, duration: 1.7 }), style34: split => ({ styleClass: 'prism-split', x: () => gsap.utils.random(-15, 15), opacity: 0, filter: 'hue-rotate(90deg) saturate(200%)', ease: 'power2.out', stagger: { amount: 1, from: 'center' }, duration: 1.4 }), style35: split => ({ styleClass: 'smoke-reveal', y: 40, opacity: 0, filter: 'blur(8px)', ease: 'power3.out', stagger: 0.08, duration: 1.9 }), style36: split => ({ styleClass: 'quantum-shift', scale: () => gsap.utils.random(0.5, 1.5), x: () => gsap.utils.random(-40, 40), y: () => gsap.utils.random(-40, 40), rotation: () => gsap.utils.random(-90, 90), opacity: 0, ease: 'power3.out', stagger: { amount: 1.8, from: 'random' }, duration: 2.1 }) }; // --------------------------------------------------- // 5. Core Animation Function // --------------------------------------------------- function animateElement(el, styleNum, opts = {}) { return new Promise(resolve => { document.fonts.ready.then(() => { const split = new SplitText(el, { type: 'lines,words,chars', tag: 'span', linesClass: 'split-line', wordsClass: 'split-word', charsClass: 'split-char' }); const key = `style${styleNum}`; const baseFn = animationConfigs[key]; if (!baseFn) return resolve(); const baseCfg = baseFn(split); const dataCfg = parseDataAttrs(el); const tweenVars = Object.assign({}, baseCfg, dataCfg, opts); const styleClass = tweenVars.styleClass; delete tweenVars.styleClass; el.classList.add(`textify-style${styleNum}`, styleClass); gsap.from(split.chars, { ...tweenVars, scrollTrigger: Object.assign({ trigger: el }, defaultConfig.scrollTrigger), onComplete: resolve }); }); }); } // --------------------------------------------------- // 6. Public API Methods // --------------------------------------------------- const Textify = { version: '4.0.0', /** * Initialize and auto-animate all matching elements. */ init(config = {}) { Object.assign(defaultConfig, config); injectBaseCSS(); document.querySelectorAll(defaultConfig.selector).forEach(el => { const m = el.className.match(/textify-style(\d+)/); if (m) animateElement(el, +m[1]); }); return this; }, /** * Animate a single element on demand. */ animate(selectorOrEl, styleNum, opts) { const el = typeof selectorOrEl === 'string' ? document.querySelector(selectorOrEl) : selectorOrEl; if (!el) return Promise.reject('Element not found'); return animateElement(el, styleNum, opts); }, /** * Animate all elements matching selector. */ animateAll(selector = defaultConfig.selector, styleNum) { const els = document.querySelectorAll(selector); return Promise.all(Array.from(els).map(el => { const num = styleNum || (el.className.match(/textify-style(\d+)/) || [])[1]; return num ? animateElement(el, +num) : Promise.resolve(); })); }, /** * Check if an element has been split into characters. */ isAnimated(selectorOrEl) { const el = typeof selectorOrEl === 'string' ? document.querySelector(selectorOrEl) : selectorOrEl; return !!(el && el.querySelector('.split-char')); }, /** * Retrieve all available style configurations. */ getStyles() { return Object.keys(animationConfigs).map(key => ({ name: key, config: animationConfigs[key] })); }, /** * Add or override an animation style. */ addStyle(name, configFn) { animationConfigs[name] = configFn; return this; } }; // --------------------------------------------------- // 7. Auto-Initialization on DOM Ready // --------------------------------------------------- document.addEventListener('DOMContentLoaded', () => { if (document.querySelector('[data-textify="auto"]')) { Textify.init(); } }); return Textify; })));