1
- /* global setTimeout:false */
2
1
import React from "react" ;
3
2
import * as d3Ease from "victory-vendor/d3-ease" ;
4
3
import { victoryInterpolator } from "./util" ;
5
4
import TimerContext from "../victory-util/timer-context" ;
6
- import isEqual from "react-fast-compare" ;
7
- import type Timer from "../victory-util/timer" ;
8
5
9
6
/**
10
7
* Single animation object to interpolate
@@ -15,6 +12,7 @@ export type AnimationStyle = { [key: string]: string | number };
15
12
*/
16
13
17
14
export type AnimationData = AnimationStyle | AnimationStyle [ ] ;
15
+
18
16
export type AnimationEasing =
19
17
| "back"
20
18
| "backIn"
@@ -58,17 +56,19 @@ export type AnimationEasing =
58
56
| "sinInOut" ;
59
57
60
58
export interface VictoryAnimationProps {
61
- children : ( style : AnimationStyle , info : AnimationInfo ) => React . ReactNode ;
59
+ children : ( style : AnimationStyle , info : AnimationInfo ) => React . ReactElement ;
62
60
duration ?: number ;
63
61
easing ?: AnimationEasing ;
64
62
delay ?: number ;
65
63
onEnd ?: ( ) => void ;
66
64
data : AnimationData ;
67
65
}
66
+
68
67
export interface VictoryAnimationState {
69
68
data : AnimationStyle ;
70
69
animationInfo : AnimationInfo ;
71
70
}
71
+
72
72
export interface AnimationInfo {
73
73
progress : number ;
74
74
animating : boolean ;
@@ -79,169 +79,138 @@ export interface VictoryAnimation {
79
79
context : React . ContextType < typeof TimerContext > ;
80
80
}
81
81
82
- export class VictoryAnimation extends React . Component <
83
- VictoryAnimationProps ,
84
- VictoryAnimationState
85
- > {
86
- static displayName = "VictoryAnimation" ;
87
-
88
- static defaultProps = {
89
- data : { } ,
90
- delay : 0 ,
91
- duration : 1000 ,
92
- easing : "quadInOut" ,
93
- } ;
82
+ /** d3-ease changed the naming scheme for ease from "linear" -> "easeLinear" etc. */
83
+ const formatAnimationName = ( name : AnimationEasing ) => {
84
+ const capitalizedName = name . charAt ( 0 ) . toUpperCase ( ) + name . slice ( 1 ) ;
85
+ return `ease${ capitalizedName } ` ;
86
+ } ;
94
87
95
- static contextType = TimerContext ;
96
- private interpolator : null | ( ( value : number ) => AnimationStyle ) ;
97
- private queue : AnimationStyle [ ] ;
98
- private ease : any ;
99
- private timer : Timer ;
100
- private loopID ?: number ;
101
-
102
- constructor ( props , context ) {
103
- super ( props , context ) ;
104
- /* defaults */
105
- this . state = {
106
- data : Array . isArray ( this . props . data )
107
- ? this . props . data [ 0 ]
108
- : this . props . data ,
109
- animationInfo : {
110
- progress : 0 ,
111
- animating : false ,
112
- } ,
113
- } ;
114
- this . interpolator = null ;
115
- this . queue = Array . isArray ( this . props . data ) ? this . props . data . slice ( 1 ) : [ ] ;
116
- /* build easing function */
117
- this . ease = d3Ease [ this . toNewName ( this . props . easing ) ] ;
118
- this . timer = this . context . animationTimer ;
119
- }
120
-
121
- componentDidMount ( ) {
88
+ const DEFAULT_DURATION = 1000 ;
89
+
90
+ export const VictoryAnimation = ( {
91
+ duration = DEFAULT_DURATION ,
92
+ easing = "quadInOut" ,
93
+ delay = 0 ,
94
+ data,
95
+ children,
96
+ onEnd,
97
+ } : VictoryAnimationProps ) => {
98
+ const [ state , setState ] = React . useState < VictoryAnimationState > ( {
99
+ data : Array . isArray ( data ) ? data [ 0 ] : data ,
100
+ animationInfo : {
101
+ progress : 0 ,
102
+ animating : false ,
103
+ } ,
104
+ } ) ;
105
+
106
+ const timer = React . useContext ( TimerContext ) . animationTimer ;
107
+ const queue = React . useRef < AnimationStyle [ ] > (
108
+ Array . isArray ( data ) ? data . slice ( 1 ) : [ ] ,
109
+ ) ;
110
+ const interpolator = React . useRef < null | ( ( value : number ) => AnimationStyle ) > (
111
+ null ,
112
+ ) ;
113
+ const loopID = React . useRef < number | undefined > ( undefined ) ;
114
+ const ease = d3Ease [ formatAnimationName ( easing ) ] ;
115
+
116
+ React . useEffect ( ( ) => {
122
117
// Length check prevents us from triggering `onEnd` in `traverseQueue`.
123
- if ( this . queue . length ) {
124
- this . traverseQueue ( ) ;
118
+ if ( queue . current . length ) {
119
+ traverseQueue ( ) ;
125
120
}
126
- }
127
-
128
- componentDidUpdate ( prevProps ) {
129
- const equalProps = isEqual ( this . props , prevProps ) ;
130
- if ( ! equalProps ) {
131
- /* If the previous animation didn't finish, force it to complete before starting a new one */
132
- if (
133
- this . interpolator &&
134
- this . state . animationInfo &&
135
- this . state . animationInfo . progress < 1
136
- ) {
137
- // eslint-disable-next-line react/no-did-update-set-state
138
- this . setState ( {
139
- data : this . interpolator ( 1 ) ,
140
- animationInfo : {
141
- progress : 1 ,
142
- animating : false ,
143
- terminating : true ,
144
- } ,
145
- } ) ;
121
+
122
+ // Clean up the animation loop
123
+ return ( ) => {
124
+ if ( loopID . current ) {
125
+ timer . unsubscribe ( loopID . current ) ;
146
126
} else {
147
- /* cancel existing loop if it exists */
148
- this . timer . unsubscribe ( this . loopID ) ;
149
- /* If an object was supplied */
150
- if ( ! Array . isArray ( this . props . data ) ) {
151
- // Replace the tween queue. Could set `this.queue = [nextProps.data]`,
152
- // but let's reuse the same array.
153
- this . queue . length = 0 ;
154
- this . queue . push ( this . props . data ) ;
155
- /* If an array was supplied */
156
- } else {
157
- /* Extend the tween queue */
158
- this . queue . push ( ...this . props . data ) ;
159
- }
160
- /* Start traversing the tween queue */
161
- this . traverseQueue ( ) ;
127
+ timer . stop ( ) ;
162
128
}
163
- }
164
- }
129
+ } ;
130
+ // eslint-disable-next-line react-hooks/exhaustive-deps
131
+ } , [ ] ) ;
165
132
166
- componentWillUnmount ( ) {
167
- if ( this . loopID ) {
168
- this . timer . unsubscribe ( this . loopID ) ;
133
+ React . useEffect ( ( ) => {
134
+ // If the previous animation didn't finish, force it to complete before starting a new one
135
+ if (
136
+ interpolator . current &&
137
+ state . animationInfo &&
138
+ state . animationInfo . progress < 1
139
+ ) {
140
+ setState ( {
141
+ data : interpolator . current ( 1 ) ,
142
+ animationInfo : {
143
+ progress : 1 ,
144
+ animating : false ,
145
+ terminating : true ,
146
+ } ,
147
+ } ) ;
169
148
} else {
170
- this . timer . stop ( ) ;
149
+ // Cancel existing loop if it exists
150
+ timer . unsubscribe ( loopID . current ) ;
151
+ // Set the tween queue to the new data
152
+ queue . current = Array . isArray ( data ) ? data : [ data ] ;
153
+ // Start traversing the tween queue
154
+ traverseQueue ( ) ;
171
155
}
172
- }
173
-
174
- toNewName ( ease ) {
175
- // d3-ease changed the naming scheme for ease from "linear" -> "easeLinear" etc.
176
- const capitalize = ( s ) => s && s [ 0 ] . toUpperCase ( ) + s . slice ( 1 ) ;
177
- return `ease${ capitalize ( ease ) } ` ;
178
- }
179
-
180
- /* Traverse the tween queue */
181
- traverseQueue ( ) {
182
- if ( this . queue . length ) {
183
- /* Get the next index */
184
- const data = this . queue [ 0 ] ;
185
- /* compare cached version to next props */
186
- this . interpolator = victoryInterpolator ( this . state . data , data ) ;
187
- /* reset step to zero */
188
- if ( this . props . delay ) {
156
+ // eslint-disable-next-line react-hooks/exhaustive-deps
157
+ } , [ data ] ) ;
158
+
159
+ const traverseQueue = ( ) => {
160
+ if ( queue . current . length ) {
161
+ const nextData = queue . current [ 0 ] ;
162
+
163
+ // Compare cached version to next props
164
+ interpolator . current = victoryInterpolator ( state . data , nextData ) ;
165
+
166
+ // Reset step to zero
167
+ if ( delay ) {
189
168
setTimeout ( ( ) => {
190
- this . loopID = this . timer . subscribe (
191
- this . functionToBeRunEachFrame ,
192
- this . props . duration ! ,
193
- ) ;
194
- } , this . props . delay ) ;
169
+ loopID . current = timer . subscribe ( functionToBeRunEachFrame , duration ) ;
170
+ } , delay ) ;
195
171
} else {
196
- this . loopID = this . timer . subscribe (
197
- this . functionToBeRunEachFrame ,
198
- this . props . duration ! ,
199
- ) ;
172
+ loopID . current = timer . subscribe ( functionToBeRunEachFrame , duration ) ;
200
173
}
201
- } else if ( this . props . onEnd ) {
202
- this . props . onEnd ( ) ;
174
+ } else if ( onEnd ) {
175
+ onEnd ( ) ;
203
176
}
204
- }
205
- /* every frame we... */
206
- functionToBeRunEachFrame = ( elapsed , duration ) => {
207
- /*
208
- step can generate imprecise values, sometimes greater than 1
209
- if this happens set the state to 1 and return, cancelling the timer
210
- */
211
- const animationDuration =
212
- duration !== undefined ? duration : this . props . duration ;
213
- const step = animationDuration ? elapsed / animationDuration : 1 ;
177
+ } ;
178
+
179
+ const functionToBeRunEachFrame = ( elapsed : number ) => {
180
+ if ( ! interpolator . current ) return ;
181
+
182
+ // Step can generate imprecise values, sometimes greater than 1
183
+ // if this happens set the state to 1 and return, cancelling the timer
184
+ const step = duration ? elapsed / duration : 1 ;
185
+
214
186
if ( step >= 1 ) {
215
- this . setState ( {
216
- data : this . interpolator ! ( 1 ) ,
187
+ setState ( {
188
+ data : interpolator . current ( 1 ) ,
217
189
animationInfo : {
218
190
progress : 1 ,
219
191
animating : false ,
220
192
terminating : true ,
221
193
} ,
222
194
} ) ;
223
- if ( this . loopID ) {
224
- this . timer . unsubscribe ( this . loopID ) ;
195
+ if ( loopID . current ) {
196
+ timer . unsubscribe ( loopID . current ) ;
225
197
}
226
- this . queue . shift ( ) ;
227
- this . traverseQueue ( ) ;
198
+ queue . current . shift ( ) ;
199
+ traverseQueue ( ) ;
228
200
return ;
229
201
}
230
- /*
231
- if we're not at the end of the timer, set the state by passing
232
- current step value that's transformed by the ease function to the
233
- interpolator, which is cached for performance whenever props are received
234
- */
235
- this . setState ( {
236
- data : this . interpolator ! ( this . ease ( step ) ) ,
202
+
203
+ // If we're not at the end of the timer, set the state by passing
204
+ // current step value that's transformed by the ease function to the
205
+ // interpolator, which is cached for performance whenever props are received
206
+ setState ( {
207
+ data : interpolator . current ( ease ( step ) ) ,
237
208
animationInfo : {
238
209
progress : step ,
239
210
animating : step < 1 ,
240
211
} ,
241
212
} ) ;
242
213
} ;
243
214
244
- render ( ) {
245
- return this . props . children ( this . state . data , this . state . animationInfo ) ;
246
- }
247
- }
215
+ return children ( state . data , state . animationInfo ) ;
216
+ } ;
0 commit comments