@@ -4,7 +4,7 @@ import { FrameRequestCallback } from 'shared/types'
4
4
import { Controller , FrameUpdate } from './Controller'
5
5
import { ActiveAnimation } from './types/spring'
6
6
7
- type FrameUpdater = ( this : FrameLoop ) => boolean
7
+ type FrameUpdater = ( this : FrameLoop , time ?: number ) => boolean
8
8
type FrameListener = ( this : FrameLoop , updates : FrameUpdate [ ] ) => void
9
9
type RequestFrameFn = ( cb : FrameRequestCallback ) => number | void
10
10
@@ -38,6 +38,8 @@ export class FrameLoop {
38
38
*/
39
39
requestFrame : RequestFrameFn
40
40
41
+ lastTime ?: number
42
+
41
43
constructor ( {
42
44
update,
43
45
onFrame,
@@ -62,34 +64,44 @@ export class FrameLoop {
62
64
63
65
this . update =
64
66
( update && update . bind ( this ) ) ||
65
- ( ( ) => {
67
+ ( ( time ?: number ) => {
66
68
if ( this . idle ) {
67
69
return false
68
70
}
69
71
70
- // Update the animations.
71
- const updates : FrameUpdate [ ] = [ ]
72
- for ( const id of Array . from ( this . controllers . keys ( ) ) ) {
73
- let idle = true
74
- const ctrl = this . controllers . get ( id ) !
75
- const changes : FrameUpdate [ 2 ] = ctrl . props . onFrame ? [ ] : null
76
- for ( const config of ctrl . configs ) {
77
- if ( config . idle ) continue
78
- if ( this . advance ( config , changes ) ) {
79
- idle = false
72
+ time = time !== void 0 ? time : performance . now ( )
73
+ this . lastTime = this . lastTime !== void 0 ? this . lastTime : time
74
+ let dt = time - this . lastTime !
75
+
76
+ // http://gafferongames.com/game-physics/fix-your-timestep/
77
+ if ( dt > 64 ) dt = 64
78
+
79
+ if ( dt > 0 ) {
80
+ // Update the animations.
81
+ const updates : FrameUpdate [ ] = [ ]
82
+ for ( const id of Array . from ( this . controllers . keys ( ) ) ) {
83
+ let idle = true
84
+ const ctrl = this . controllers . get ( id ) !
85
+ const changes : FrameUpdate [ 2 ] = ctrl . props . onFrame ? [ ] : null
86
+ for ( const config of ctrl . configs ) {
87
+ if ( config . idle ) continue
88
+ if ( this . advance ( dt , config , changes ) ) {
89
+ idle = false
90
+ }
91
+ }
92
+ if ( idle || changes ) {
93
+ updates . push ( [ id , idle , changes ] )
80
94
}
81
95
}
82
- if ( idle || changes ) {
83
- updates . push ( [ id , idle , changes ] )
84
- }
85
- }
86
96
87
- // Notify the controllers!
88
- this . onFrame ( updates )
97
+ // Notify the controllers!
98
+ this . onFrame ( updates )
99
+ this . lastTime = time
89
100
90
- // Are we done yet?
91
- if ( ! this . controllers . size ) {
92
- return ! ( this . idle = true )
101
+ // Are we done yet?
102
+ if ( ! this . controllers . size ) {
103
+ return ! ( this . idle = true )
104
+ }
93
105
}
94
106
95
107
// Keep going.
@@ -102,6 +114,7 @@ export class FrameLoop {
102
114
this . controllers . set ( ctrl . id , ctrl )
103
115
if ( this . idle ) {
104
116
this . idle = false
117
+ this . lastTime = undefined
105
118
this . requestFrame ( this . update )
106
119
}
107
120
}
@@ -111,9 +124,11 @@ export class FrameLoop {
111
124
}
112
125
113
126
/** Advance an animation forward one frame. */
114
- advance ( config : ActiveAnimation , changes : FrameUpdate [ 2 ] ) : boolean {
115
- const time = G . now ( )
116
-
127
+ advance (
128
+ dt : number ,
129
+ config : ActiveAnimation ,
130
+ changes : FrameUpdate [ 2 ]
131
+ ) : boolean {
117
132
let active = false
118
133
let changed = false
119
134
for ( let i = 0 ; i < config . animatedValues . length ; i ++ ) {
@@ -125,86 +140,90 @@ export class FrameLoop {
125
140
const target : any = to instanceof Animated ? to : null
126
141
if ( target ) to = target . getValue ( )
127
142
143
+ const from : any = config . fromValues [ i ]
144
+
128
145
// Jump to end value for immediate animations
129
- if ( config . immediate ) {
146
+ if (
147
+ config . immediate ||
148
+ typeof from === 'string' ||
149
+ typeof to === 'string'
150
+ ) {
130
151
animated . setValue ( to )
131
152
animated . done = true
132
153
continue
133
154
}
134
155
135
- const from : any = config . fromValues [ i ]
136
- const startTime = animated . startTime !
156
+ const startTime = animated . startTime
157
+ const elapsed = ( animated . elapsedTime += dt )
137
158
138
- // Break animation when string values are involved
139
- if ( typeof from === 'string' || typeof to === 'string' ) {
140
- animated . setValue ( to )
141
- animated . done = true
142
- continue
143
- }
159
+ const v0 = Array . isArray ( config . initialVelocity )
160
+ ? config . initialVelocity [ i ]
161
+ : config . initialVelocity
162
+
163
+ const precision =
164
+ config . precision || Math . min ( 1 , Math . abs ( to - from ) * 0.001 )
144
165
145
166
let finished = false
146
167
let position = animated . lastPosition
147
- let velocity = Array . isArray ( config . initialVelocity )
148
- ? config . initialVelocity [ i ]
149
- : config . initialVelocity
168
+
169
+ let velocity : number
150
170
151
171
// Duration easing
152
172
if ( config . duration !== void 0 ) {
153
173
position =
154
- from +
155
- config . easing ! ( ( time - startTime ) / config . duration ) * ( to - from )
174
+ from + config . easing ! ( elapsed / config . duration ) * ( to - from )
175
+
176
+ velocity = ( position - animated . lastPosition ) / dt
156
177
157
- finished = time >= startTime + config . duration
178
+ finished = G . now ( ) >= startTime + config . duration
158
179
}
159
180
// Decay easing
160
181
else if ( config . decay ) {
161
182
const decay = config . decay === true ? 0.998 : config . decay
162
- position =
163
- from +
164
- ( velocity / ( 1 - decay ) ) *
165
- ( 1 - Math . exp ( - ( 1 - decay ) * ( time - startTime ) ) )
183
+ const e = Math . exp ( - ( 1 - decay ) * elapsed )
184
+
185
+ position = from + ( v0 / ( 1 - decay ) ) * ( 1 - e )
186
+ // derivative of position
187
+ velocity = v0 * e
166
188
167
189
finished = Math . abs ( animated . lastPosition - position ) < 0.1
168
190
if ( finished ) to = position
169
191
}
170
192
// Spring easing
171
193
else {
172
- let lastTime = animated . lastTime ! == void 0 ? animated . lastTime : time
173
- if ( animated . lastVelocity !== void 0 ) {
174
- velocity = animated . lastVelocity
175
- }
194
+ velocity = animated . lastVelocity == null ? v0 : animated . lastVelocity
195
+
196
+ const step = 0.05 / config . w0
197
+ const numSteps = Math . ceil ( dt / step )
176
198
177
- // If we lost a lot of frames just jump to the end.
178
- if ( time > lastTime + 64 ) lastTime = time
179
- // http://gafferongames.com/game-physics/fix-your-timestep/
180
- const numSteps = Math . floor ( time - lastTime )
181
199
for ( let n = 0 ; n < numSteps ; ++ n ) {
182
- const force = - config . tension ! * ( position - to )
183
- const damping = - config . friction ! * velocity
184
- const acceleration = ( force + damping ) / config . mass !
185
- velocity = velocity + ( acceleration * 1 ) / 1000
186
- position = position + ( velocity * 1 ) / 1000
200
+ const springForce = - config . tension ! * 0.000001 * ( position - to )
201
+ const dampingForce = - config . friction ! * 0.001 * velocity
202
+ const acceleration = ( springForce + dampingForce ) / config . mass ! // pt/ms^2
203
+ velocity = velocity + acceleration * step // pt/ms
204
+ position = position + velocity * step
187
205
}
188
206
189
- animated . lastTime = time
190
- animated . lastVelocity = velocity
191
-
192
207
// Conditions for stopping the spring animation
193
- const isOvershooting =
194
- config . clamp && config . tension !== 0
208
+ const isBouncing =
209
+ config . clamp !== false && config . tension !== 0
195
210
? from < to
196
- ? position > to
197
- : position < to
211
+ ? position > to && velocity > 0
212
+ : position < to && velocity < 0
198
213
: false
199
- const isVelocity = Math . abs ( velocity ) <= config . precision !
214
+
215
+ if ( isBouncing ) {
216
+ velocity =
217
+ - velocity * ( config . clamp === true ? 0 : ( config . clamp as any ) )
218
+ }
219
+
220
+ const isVelocity = Math . abs ( velocity ) <= precision
200
221
const isDisplacement =
201
- config . tension !== 0
202
- ? Math . abs ( to - position ) <= config . precision !
203
- : true
222
+ config . tension !== 0 ? Math . abs ( to - position ) <= precision : true
204
223
205
- finished = isOvershooting || ( isVelocity && isDisplacement )
224
+ finished =
225
+ ( isBouncing && velocity === 0 ) || ( isVelocity && isDisplacement )
206
226
}
207
-
208
227
// Trails aren't done until their parents conclude
209
228
if ( finished && ! ( target && ! target . done ) ) {
210
229
// Ensure that we end up with a round value
@@ -216,6 +235,7 @@ export class FrameLoop {
216
235
217
236
animated . setValue ( position )
218
237
animated . lastPosition = position
238
+ animated . lastVelocity = velocity
219
239
}
220
240
221
241
if ( changes && changed ) {
0 commit comments