Source

AnimatedText.js

  1. /**
  2. * @fileoverview Animated Text Component for React applications
  3. * @module AnimatedText
  4. * @requires react
  5. * @version 1.0.0
  6. */
  7. import React, { useState, useEffect } from 'react';
  8. /**
  9. * @typedef {Object} AnimatedTextConfig
  10. * @property {number} [speed=100] - Animation speed in milliseconds
  11. * @property {boolean} [loop=false] - Whether to loop the animation
  12. * @property {number} [delay=0] - Delay before animation starts
  13. * @property {string} [color='currentColor'] - Text color
  14. */
  15. /**
  16. * @typedef {Object} AnimatedTextProps
  17. * @property {string} text - The text to animate
  18. * @property {('typewriter'|'fadeIn'|'bounce'|'glitch'|'rainbow')} [effect='typewriter'] - Animation effect
  19. * @property {AnimatedTextConfig} [config] - Animation configuration
  20. * @property {Object} [styles] - Custom CSS styles
  21. */
  22. class TextErrorBoundary extends React.Component {
  23. state = { hasError: false };
  24. static getDerivedStateFromError(error) {
  25. return { hasError: true };
  26. }
  27. render() {
  28. if (this.state.hasError) {
  29. return <span>Animation failed to load.</span>;
  30. }
  31. return this.props.children;
  32. }
  33. }
  34. /**
  35. * AnimatedText Component
  36. * @param {AnimatedTextProps} props - Component props
  37. * @returns {React.ReactElement} Rendered component
  38. */
  39. const AnimatedText = ({
  40. text = '',
  41. effect = 'typewriter',
  42. config = {},
  43. styles = {}
  44. }) => {
  45. const defaultConfig = {
  46. speed: 100,
  47. loop: false,
  48. delay: 0,
  49. color: 'currentColor',
  50. ...config
  51. };
  52. const defaultStyles = {
  53. base: {
  54. display: 'inline-block',
  55. fontFamily: 'inherit',
  56. color: defaultConfig.color,
  57. },
  58. typewriter: {
  59. whiteSpace: 'pre',
  60. overflow: 'hidden',
  61. borderRight: '0.15em solid currentColor',
  62. },
  63. fadeIn: {
  64. opacity: 0,
  65. animation: 'fadeIn 2s forwards',
  66. },
  67. bounce: {
  68. display: 'inline-block',
  69. },
  70. glitch: {
  71. position: 'relative',
  72. animation: 'glitch 1s linear infinite',
  73. },
  74. rainbow: {
  75. background: 'linear-gradient(to right, #6666ff, #0099ff , #00ff00, #ff3399, #6666ff)',
  76. backgroundSize: '400%',
  77. backgroundClip: 'text',
  78. WebkitBackgroundClip: 'text',
  79. WebkitTextFillColor: 'transparent',
  80. animation: 'rainbow 8s ease infinite',
  81. }
  82. };
  83. const effects = {
  84. typewriter: (text) => {
  85. const [displayText, setDisplayText] = useState('');
  86. const [isAnimating, setIsAnimating] = useState(true);
  87. useEffect(() => {
  88. if (!text) {
  89. setIsAnimating(false);
  90. return;
  91. }
  92. const startAnimation = () => {
  93. let i = 0;
  94. setDisplayText('');
  95. setIsAnimating(true);
  96. const timer = setInterval(() => {
  97. try {
  98. if (i < text.length) {
  99. setDisplayText(prev => prev + text[i]);
  100. i++;
  101. } else {
  102. setIsAnimating(false);
  103. if (defaultConfig.loop) {
  104. setTimeout(startAnimation, defaultConfig.delay);
  105. }
  106. clearInterval(timer);
  107. }
  108. } catch (error) {
  109. console.error('Animation error:', error);
  110. clearInterval(timer);
  111. setIsAnimating(false);
  112. }
  113. }, defaultConfig.speed);
  114. return timer;
  115. };
  116. const timer = setTimeout(startAnimation, defaultConfig.delay);
  117. return () => clearTimeout(timer);
  118. }, [text, defaultConfig.loop, defaultConfig.speed, defaultConfig.delay]);
  119. return (
  120. <span
  121. style={{
  122. ...defaultStyles.base,
  123. ...defaultStyles.typewriter,
  124. ...styles,
  125. borderRight: isAnimating ? '0.15em solid currentColor' : 'none'
  126. }}
  127. aria-label={text}
  128. >
  129. {displayText}
  130. </span>
  131. );
  132. },
  133. fadeIn: (text) => (
  134. <span
  135. style={{
  136. ...defaultStyles.base,
  137. ...defaultStyles.fadeIn,
  138. ...styles
  139. }}
  140. >
  141. {text}
  142. </span>
  143. ),
  144. bounce: (text) => (
  145. <span style={{ ...defaultStyles.base, ...styles }}>
  146. {text.split('').map((char, i) => (
  147. <span
  148. key={i}
  149. style={{
  150. ...defaultStyles.bounce,
  151. animation: `bounce 0.5s ease infinite`,
  152. animationDelay: `${i * 0.1}s`
  153. }}
  154. >
  155. {char}
  156. </span>
  157. ))}
  158. </span>
  159. ),
  160. glitch: (text) => (
  161. <span style={{ ...defaultStyles.base, ...defaultStyles.glitch, ...styles }}>
  162. {text}
  163. <span className="glitch-effect" data-text={text}></span>
  164. </span>
  165. ),
  166. rainbow: (text) => (
  167. <span style={{ ...defaultStyles.base, ...defaultStyles.rainbow, ...styles }}>
  168. {text}
  169. </span>
  170. )
  171. };
  172. useEffect(() => {
  173. const styleSheet = document.createElement('style');
  174. styleSheet.textContent = `
  175. @keyframes fadeIn {
  176. to { opacity: 1; }
  177. }
  178. @keyframes bounce {
  179. 0%, 100% { transform: translateY(0); }
  180. 50% { transform: translateY(-10px); }
  181. }
  182. @keyframes glitch {
  183. 2%, 64% { transform: translate(2px,0) skew(0deg); }
  184. 4%, 60% { transform: translate(-2px,0) skew(0deg); }
  185. 62% { transform: translate(0,0) skew(5deg); }
  186. }
  187. @keyframes rainbow {
  188. 0% { background-position: 0% 50%; }
  189. 50% { background-position: 100% 50%; }
  190. 100% { background-position: 0% 50%; }
  191. }
  192. `;
  193. document.head.appendChild(styleSheet);
  194. return () => document.head.removeChild(styleSheet);
  195. }, []);
  196. const safeText = typeof text === 'string' ? text : String(text || '');
  197. const safeEffect = effects.hasOwnProperty(effect) ? effect : 'typewriter';
  198. return (
  199. <TextErrorBoundary>
  200. {effects[safeEffect](safeText)}
  201. </TextErrorBoundary>
  202. );
  203. };
  204. export default AnimatedText;