@@ -22,7 +22,7 @@ const GradientHover: React.FC<GradientHoverProps> = ({
22
22
className = "" ,
23
23
style = { } ,
24
24
onClick,
25
- animationSpeed = 3 ,
25
+ animationSpeed = 5 ,
26
26
transitionDuration = 1 ,
27
27
shouldAlwaysShowGradient = true ,
28
28
} ) => {
@@ -35,26 +35,27 @@ const GradientHover: React.FC<GradientHoverProps> = ({
35
35
{ top : number ; left : number ; width : number ; height : number } | undefined
36
36
> ( undefined ) ;
37
37
38
- // Animation state for smooth trailing
38
+ // Animation state
39
39
const targetPosition = useRef < { x : number ; y : number } > ( { x : 0 , y : 0 } ) ;
40
40
const currentPosition = useRef < { x : number ; y : number } > ( { x : 0 , y : 0 } ) ;
41
41
const animationFrame = useRef < number | undefined > ( undefined ) ;
42
42
const isAnimatingToCenter = useRef ( false ) ;
43
43
const isAnimationActive = useRef ( false ) ;
44
+ const lastTime = useRef < number > ( 0 ) ;
44
45
45
46
// Ensure we have at least 2 colors for the gradient
46
47
const validColors = colors && colors . length >= 2 ? colors : DEFAULT_COLORS ;
47
48
48
- // Convert user-friendly speed (1-10) to internal speed (0.01-0.1)
49
- const internalAnimationSpeed =
50
- Math . max ( 1 , Math . min ( 10 , animationSpeed ) ) * 0.01 ;
49
+ // Calculate smoothing speed using an exponential curve
50
+ // This gives us a nice progression from very slow to fast
51
+ const smoothingSpeed = 0.00025 * Math . pow ( animationSpeed , 1.3 ) ;
51
52
52
- // Generate gradient string for CSS
53
+ // Generate gradient string
53
54
const generateGradient = ( x : string , y : string ) => {
54
55
return `radial-gradient(circle at ${ x } ${ y } , ${ validColors . join ( ", " ) } )` ;
55
56
} ;
56
57
57
- // Generate CSS variables for all gradient stops
58
+ // CSS variables for gradient
58
59
const colorStyles = {
59
60
"--gradient-colors" : validColors . join ( ", " ) ,
60
61
"--gradient-stop-last" : validColors [ validColors . length - 1 ] ,
@@ -74,7 +75,7 @@ const GradientHover: React.FC<GradientHoverProps> = ({
74
75
} as CSSProperties ;
75
76
}
76
77
77
- // Helper function to get fresh bounds
78
+ // Get fresh bounds for accurate positioning
78
79
const getFreshBounds = useCallback ( ( ) => {
79
80
if ( ref . current ) {
80
81
const rect = ref . current . getBoundingClientRect ( ) ;
@@ -92,41 +93,55 @@ const GradientHover: React.FC<GradientHoverProps> = ({
92
93
return null ;
93
94
} , [ ] ) ;
94
95
95
- // Continuous animation function - runs while hovering or animating to center
96
- const animate = useCallback ( ( ) => {
97
- if ( ! isAnimationActive . current ) {
98
- return ;
99
- }
100
-
101
- const distX = targetPosition . current . x - currentPosition . current . x ;
102
- const distY = targetPosition . current . y - currentPosition . current . y ;
96
+ // Animation loop using exponential smoothing
97
+ const animate = useCallback (
98
+ ( currentTime : number ) => {
99
+ if ( ! isAnimationActive . current ) {
100
+ return ;
101
+ }
103
102
104
- // Check if we're close enough to the target to stop animating to center
105
- if (
106
- isAnimatingToCenter . current &&
107
- Math . abs ( distX ) < 1 &&
108
- Math . abs ( distY ) < 1
109
- ) {
110
- isAnimatingToCenter . current = false ;
111
- isAnimationActive . current = false ;
112
- setMousePosition ( undefined ) ;
113
- if ( animationFrame . current ) {
114
- cancelAnimationFrame ( animationFrame . current ) ;
115
- animationFrame . current = undefined ;
103
+ // Calculate delta time for frame-independent animation
104
+ const deltaTime = lastTime . current
105
+ ? Math . min ( ( currentTime - lastTime . current ) / 16.67 , 2 )
106
+ : 1 ;
107
+ lastTime . current = currentTime ;
108
+
109
+ const distX = targetPosition . current . x - currentPosition . current . x ;
110
+ const distY = targetPosition . current . y - currentPosition . current . y ;
111
+ const distance = Math . sqrt ( distX * distX + distY * distY ) ;
112
+
113
+ // Stop animating when close to center
114
+ if ( distance < 0.5 && isAnimatingToCenter . current ) {
115
+ isAnimatingToCenter . current = false ;
116
+ isAnimationActive . current = false ;
117
+ setMousePosition ( undefined ) ;
118
+ if ( animationFrame . current ) {
119
+ cancelAnimationFrame ( animationFrame . current ) ;
120
+ animationFrame . current = undefined ;
121
+ }
122
+ return ;
116
123
}
117
- return ;
118
- }
119
124
120
- currentPosition . current . x += distX * internalAnimationSpeed ;
121
- currentPosition . current . y += distY * internalAnimationSpeed ;
125
+ // Use faster speed when returning to center
126
+ const effectiveSpeed = isAnimatingToCenter . current
127
+ ? Math . max ( smoothingSpeed * 3 , 0.003 )
128
+ : smoothingSpeed ;
122
129
123
- setMousePosition ( {
124
- x : currentPosition . current . x ,
125
- y : currentPosition . current . y ,
126
- } ) ;
130
+ const smoothingFactor = 1 - Math . pow ( 1 - effectiveSpeed * 10 , deltaTime ) ;
127
131
128
- animationFrame . current = requestAnimationFrame ( animate ) ;
129
- } , [ internalAnimationSpeed ] ) ;
132
+ // Move toward target by a fraction of the distance
133
+ currentPosition . current . x += distX * smoothingFactor ;
134
+ currentPosition . current . y += distY * smoothingFactor ;
135
+
136
+ setMousePosition ( {
137
+ x : currentPosition . current . x ,
138
+ y : currentPosition . current . y ,
139
+ } ) ;
140
+
141
+ animationFrame . current = requestAnimationFrame ( animate ) ;
142
+ } ,
143
+ [ smoothingSpeed , isAnimatingToCenter ]
144
+ ) ;
130
145
131
146
// Start/stop animation based on hover state
132
147
useEffect ( ( ) => {
@@ -135,6 +150,7 @@ const GradientHover: React.FC<GradientHoverProps> = ({
135
150
! animationFrame . current
136
151
) {
137
152
isAnimationActive . current = true ;
153
+ lastTime . current = 0 ;
138
154
animationFrame . current = requestAnimationFrame ( animate ) ;
139
155
} else if (
140
156
! isHovering &&
@@ -147,6 +163,7 @@ const GradientHover: React.FC<GradientHoverProps> = ({
147
163
}
148
164
} , [ isHovering , animate ] ) ;
149
165
166
+ // Debounced bounds updater
150
167
const debouncedStoreElementBounds = useRef (
151
168
debounce ( ( element : HTMLElement ) => {
152
169
if ( element ) {
@@ -161,19 +178,15 @@ const GradientHover: React.FC<GradientHoverProps> = ({
161
178
} , 100 )
162
179
) ;
163
180
164
- // Update bounds on various events
181
+ // Update bounds on resize/scroll
165
182
useEffect ( ( ) => {
166
183
const nodeEl = ref . current ;
167
- if ( ! nodeEl ) {
168
- return ;
169
- }
184
+ if ( ! nodeEl ) return ;
170
185
171
186
const updateBounds = ( ) => debouncedStoreElementBounds . current ( nodeEl ) ;
172
187
173
- // Initial bounds calculation
174
188
updateBounds ( ) ;
175
189
176
- // Listen to various events that can change element position
177
190
window . addEventListener ( "resize" , updateBounds ) ;
178
191
window . addEventListener ( "scroll" , updateBounds , { passive : true } ) ;
179
192
window . addEventListener ( "orientationchange" , updateBounds ) ;
@@ -185,18 +198,16 @@ const GradientHover: React.FC<GradientHoverProps> = ({
185
198
} ;
186
199
} , [ ] ) ;
187
200
188
- // Intersection observer for additional bounds updates
201
+ // Intersection observer for visibility changes
189
202
useEffect ( ( ) => {
190
203
const nodeEl = ref . current ;
191
- if ( ! nodeEl ) {
192
- return ;
193
- }
204
+ if ( ! nodeEl ) return ;
205
+
194
206
const fn = ( ) => debouncedStoreElementBounds . current ( nodeEl ) ;
195
207
const observer = new IntersectionObserver ( fn , { threshold : 0.1 } ) ;
196
208
observer . observe ( nodeEl ) ;
197
- return ( ) => {
198
- observer . disconnect ( ) ;
199
- } ;
209
+
210
+ return ( ) => observer . disconnect ( ) ;
200
211
} , [ ] ) ;
201
212
202
213
// Cleanup on unmount
@@ -215,7 +226,6 @@ const GradientHover: React.FC<GradientHoverProps> = ({
215
226
216
227
const handleMouseMove = ( event : MouseEvent < HTMLElement > ) => {
217
228
const { clientX, clientY } = event ;
218
- // Get fresh bounds to ensure accuracy
219
229
const freshBounds = getFreshBounds ( ) ;
220
230
if ( freshBounds ) {
221
231
targetPosition . current = {
@@ -236,10 +246,8 @@ const GradientHover: React.FC<GradientHoverProps> = ({
236
246
setIsHovering ( true ) ;
237
247
238
248
const { clientX, clientY } = event ;
239
- // Get fresh bounds to ensure accuracy
240
249
const freshBounds = getFreshBounds ( ) ;
241
250
if ( freshBounds ) {
242
- // Update stored bounds
243
251
setBounds ( {
244
252
top : freshBounds . viewportTop ,
245
253
left : freshBounds . viewportLeft ,
@@ -252,25 +260,22 @@ const GradientHover: React.FC<GradientHoverProps> = ({
252
260
y : clientY - freshBounds . viewportTop ,
253
261
} ;
254
262
255
- // Set the target position to the cursor
256
263
targetPosition . current = targetPos ;
257
264
258
- // If we don't have a current position yet or if we're starting fresh,
259
- // start from center to ensure smooth animation
265
+ // Start from center if no current position
260
266
if ( currentPosition . current . x === 0 && currentPosition . current . y === 0 ) {
261
267
currentPosition . current = {
262
268
x : freshBounds . width / 2 ,
263
269
y : freshBounds . height / 2 ,
264
270
} ;
265
- // Set initial mouse position to center as well
266
271
setMousePosition ( {
267
272
x : freshBounds . width / 2 ,
268
273
y : freshBounds . height / 2 ,
269
274
} ) ;
270
275
}
271
276
272
- // Start the animation to smoothly move to the target position
273
277
isAnimationActive . current = true ;
278
+ lastTime . current = 0 ;
274
279
if ( ! animationFrame . current ) {
275
280
animationFrame . current = requestAnimationFrame ( animate ) ;
276
281
}
@@ -281,26 +286,27 @@ const GradientHover: React.FC<GradientHoverProps> = ({
281
286
setIsHovering ( false ) ;
282
287
const freshBounds = getFreshBounds ( ) ;
283
288
if ( freshBounds ) {
284
- // Update stored bounds
285
289
setBounds ( {
286
290
top : freshBounds . viewportTop ,
287
291
left : freshBounds . viewportLeft ,
288
292
width : freshBounds . width ,
289
293
height : freshBounds . height ,
290
294
} ) ;
291
295
292
- // Set target to center and start animation back to center
296
+ // Animate back to center
293
297
targetPosition . current = {
294
298
x : freshBounds . width / 2 ,
295
299
y : freshBounds . height / 2 ,
296
300
} ;
297
301
isAnimatingToCenter . current = true ;
298
302
isAnimationActive . current = true ;
303
+ lastTime . current = 0 ;
304
+
299
305
if ( ! animationFrame . current ) {
300
306
animationFrame . current = requestAnimationFrame ( animate ) ;
301
307
}
302
308
} else {
303
- // Fallback if no bounds - just stop immediately
309
+ // Fallback - stop immediately
304
310
setMousePosition ( undefined ) ;
305
311
if ( animationFrame . current ) {
306
312
cancelAnimationFrame ( animationFrame . current ) ;
0 commit comments