1- import { gsap } from 'gsap' ;
21import SplitType from 'split-type' ;
32import { useEffect , useRef } from 'react' ;
43
@@ -110,6 +109,8 @@ export class TextAnimator {
110109 textElement : HTMLElement ;
111110 splitter ! : TextSplitter ;
112111 originalChars ! : string [ ] ;
112+ activeAnimations : globalThis . Animation [ ] = [ ] ;
113+ activeTimeouts : ReturnType < typeof setTimeout > [ ] = [ ] ;
113114
114115 constructor ( textElement : HTMLElement ) {
115116 if ( ! textElement || ! ( textElement instanceof HTMLElement ) ) {
@@ -124,7 +125,7 @@ export class TextAnimator {
124125 this . splitter = new TextSplitter ( this . textElement , {
125126 splitTypeTypes : [ 'words' , 'chars' ] ,
126127 } ) ;
127- this . originalChars = this . splitter . getChars ( ) . map ( ( char ) => char . innerHTML ) ;
128+ this . originalChars = this . splitter . getChars ( ) . map ( ( char ) => char . textContent || '' ) ;
128129 }
129130
130131 animate ( ) {
@@ -133,64 +134,83 @@ export class TextAnimator {
133134 const chars = this . splitter . getChars ( ) ;
134135
135136 chars . forEach ( ( char , position ) => {
136- const initialHTML = char . innerHTML ;
137- let repeatCount = 0 ;
138-
139- // Set initial state
140- gsap . set ( char , {
141- opacity : 1 ,
142- display : 'inline-block' ,
143- position : 'relative' ,
144- } ) ;
145-
146- gsap . fromTo (
147- char ,
148- {
149- opacity : 1 ,
150- } ,
151- {
152- duration : 0.1 , // Increased duration
153- ease : 'power2.out' ,
154- onStart : ( ) => {
155- gsap . set ( char , {
156- fontFamily : 'Cash Sans Mono' ,
157- fontWeight : 300 ,
158- color : '#666' , // Add color change
159- } ) ;
137+ const initialText = char . textContent || '' ;
138+
139+ char . style . opacity = '1' ;
140+ char . style . display = 'inline-block' ;
141+ char . style . position = 'relative' ;
142+
143+ const animation = char . animate (
144+ [
145+ {
146+ opacity : 1 ,
147+ color : '#666' ,
148+ fontFamily : 'Cash Sans Mono' ,
149+ fontWeight : '300' ,
160150 } ,
161- onComplete : ( ) => {
162- gsap . set ( char , {
163- innerHTML : initialHTML ,
164- color : '' ,
165- fontFamily : '' ,
166- opacity : 1 ,
167- } ) ;
151+ {
152+ opacity : 0.5 ,
153+ color : '#999' ,
168154 } ,
169- repeat : 2 , // Reduced repeats
170- onRepeat : ( ) => {
171- repeatCount ++ ;
172- if ( repeatCount === 1 ) {
173- gsap . set ( char , {
174- opacity : 0.5 ,
175- color : '#999' ,
176- } ) ;
177- }
155+ {
156+ opacity : 1 ,
157+ color : 'inherit' ,
158+ fontFamily : 'inherit' ,
159+ fontWeight : 'inherit' ,
178160 } ,
179- repeatRefresh : true ,
180- repeatDelay : 0.05 , // Increased delay
181- delay : position * 0.03 , // Reduced delay between chars
182- innerHTML : ( ) => lettersAndSymbols [ Math . floor ( Math . random ( ) * lettersAndSymbols . length ) ] ,
183- opacity : 1 ,
161+ ] ,
162+ {
163+ duration : 300 , // Total duration for all iterations
164+ easing : 'ease-in-out' ,
165+ delay : position * 30 , // Stagger the start of each animation
166+ iterations : 1 ,
184167 }
185168 ) ;
169+
170+ this . activeAnimations . push ( animation ) ;
171+
172+ let iteration = 0 ;
173+ const maxIterations = 2 ;
174+
175+ const animateCharacterChange = ( ) => {
176+ if ( iteration < maxIterations ) {
177+ char . textContent =
178+ lettersAndSymbols [ Math . floor ( Math . random ( ) * lettersAndSymbols . length ) ] ;
179+ const timeoutId = setTimeout ( animateCharacterChange , 100 ) ;
180+ this . activeTimeouts . push ( timeoutId ) ;
181+ iteration ++ ;
182+ } else {
183+ char . textContent = initialText ;
184+ }
185+ } ;
186+
187+ const timeoutId = setTimeout ( animateCharacterChange , position * 30 ) ;
188+ this . activeTimeouts . push ( timeoutId ) ;
189+
190+ animation . onfinish = ( ) => {
191+ char . textContent = initialText ;
192+ char . style . color = '' ;
193+ char . style . fontFamily = '' ;
194+ char . style . opacity = '1' ;
195+ } ;
186196 } ) ;
187197 }
188198
189199 reset ( ) {
200+ // Clear all timeouts
201+ this . activeTimeouts . forEach ( ( timeoutId ) => clearTimeout ( timeoutId ) ) ;
202+ this . activeTimeouts = [ ] ;
203+
204+ // Cancel all animations
205+ this . activeAnimations . forEach ( ( animation ) => animation . cancel ( ) ) ;
206+ this . activeAnimations = [ ] ;
207+
208+ // Reset text content
190209 const chars = this . splitter . getChars ( ) ;
191210 chars . forEach ( ( char , index ) => {
192- gsap . killTweensOf ( char ) ;
193- char . innerHTML = this . originalChars [ index ] ;
211+ if ( this . originalChars [ index ] ) {
212+ char . textContent = this . originalChars [ index ] ;
213+ }
194214 } ) ;
195215 }
196216}
0 commit comments