Skip to content

Commit 6fa673c

Browse files
committed
wip: round 2
1 parent fab2082 commit 6fa673c

File tree

2 files changed

+95
-82
lines changed

2 files changed

+95
-82
lines changed

src/animated/Controller.ts

Lines changed: 94 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,38 @@ import { colorNames, interpolation as interp, now } from './Globals'
1313
import { SpringProps } from '../../types/renderprops'
1414

1515
type Animation = any
16+
type AnimationMap = { [name: string]: Animation }
17+
type InterpolationMap = { [name: string]: AnimatedValue | AnimatedValueArray }
1618

17-
type ControllerProps<DS extends object> = DS &
19+
type UpdateProps<DS extends object> = DS &
1820
SpringProps<DS> & {
1921
attach?: (ctrl: Controller) => Animation
2022
}
2123

22-
type AnimationsFor<P> = { [Key in keyof P]: any }
23-
24-
type ValuesFor<P> = { [Key in keyof P]: any }
25-
26-
type InterpolationsFor<DS> = {
27-
[K in keyof DS]: DS[K] extends ArrayLike<any>
28-
? AnimatedValueArray
29-
: AnimatedValue
30-
}
31-
32-
type EndCallback = (finished?: boolean) => void
24+
type OnEnd = (finished?: boolean) => void
3325

3426
let nextId = 1
3527
class Controller<DS extends object = any> {
3628
id = nextId++
3729
idle = true
3830
guid = 0
39-
props: ControllerProps<DS> = {} as any
40-
merged: any = {}
41-
animations = {} as AnimationsFor<DS>
42-
interpolations = {} as InterpolationsFor<DS>
43-
values = {} as ValuesFor<DS>
44-
configs: any = []
31+
props: UpdateProps<DS> = {} as any
32+
merged: DS = {} as any
33+
values: DS = {} as any
34+
interpolations: InterpolationMap = {}
35+
animations: AnimationMap = {}
36+
configs: any[] = []
4537
queue: any[] = []
4638
prevQueue: any[] = []
47-
onEndQueue: EndCallback[] = []
39+
onEndQueue: OnEnd[] = []
4840
pendingCount = 0
4941

50-
// Add this controller to the frameloop
51-
private _start(onEnd?: EndCallback) {
52-
if (onEnd) this.onEndQueue.push(onEnd)
53-
if (this.idle) {
54-
this.idle = false
55-
start(this)
56-
}
57-
}
58-
59-
private _stop(finished?: boolean) {
60-
this.idle = true
61-
stop(this)
62-
if (finished && this.props.onRest) {
63-
this.props.onRest(this.merged)
64-
}
65-
if (this.onEndQueue.length) {
66-
this.onEndQueue.forEach(onEnd => onEnd(finished))
67-
this.onEndQueue = []
68-
}
69-
}
42+
getValues = () => this.interpolations
7043

7144
/** update(props)
7245
* This function filters input props and creates an array of tasks which are executed in .start()
7346
* Each task is allowed to carry a delay, which means it can execute asnychroneously */
74-
update(args: ControllerProps<DS>) {
47+
update(args: UpdateProps<DS>) {
7548
// Extract delay and the to-prop from props
7649
const { delay = 0, to, ...props } = interpolateTo(args) as any
7750

@@ -99,15 +72,15 @@ class Controller<DS extends object = any> {
9972
this.queue.sort((a, b) => a.delay - b.delay)
10073

10174
// Diff the reduced props immediately (they'll contain the from-prop and some config)
102-
if (hasKeys(props)) this.diff(props)
75+
if (hasKeys(props)) this._diff(props)
10376

10477
return this
10578
}
10679

10780
/** start()
10881
* This function either executes a queue, if present, or starts the frameloop, which animates.
10982
* The `useSpring` hooks never have > 1 update per call, because they call this every render. */
110-
start(onEnd?: EndCallback) {
83+
start(onEnd?: OnEnd) {
11184
// If a queue is present we must execute it
11285
if (this.queue.length) {
11386
const { prevQueue } = this
@@ -127,25 +100,33 @@ class Controller<DS extends object = any> {
127100
const queue = (this.prevQueue = this.queue)
128101
this.queue = prevQueue
129102

130-
// Reset delay/async tracking
103+
// Reset any queue-related state
131104
this.pendingCount = 0
105+
this.onEndQueue.length = 0
106+
107+
// Never assume that the last update always finishes last, since that's
108+
// not true when 2+ async animations have indeterminate durations.
109+
let remaining = queue.length
110+
const didEnd =
111+
onEnd &&
112+
((finished?: boolean) => {
113+
if (--remaining < 0 || guid !== this.guid) return
114+
onEnd(finished)
115+
})
132116

133117
// Go through each entry and execute it
134-
queue.forEach(({ delay, onStart, ...props }) => {
118+
queue.forEach(({ delay, ...props }) => {
135119
// Entries can be delayed, async, or immediate
136-
const async = is.arr(props.to) || is.fun(props.to)
137120
if (delay) {
138121
this.pendingCount++
139122
setTimeout(() => {
140123
if (guid === this.guid) {
141124
this.pendingCount--
142-
if (async) this.runAsync(guid, props, onEnd)
143-
else this.diff(props)._start(onEnd)
125+
this._run(guid, props, didEnd)
144126
}
145127
}, delay)
146128
} else {
147-
if (async) this.runAsync(guid, props, onEnd)
148-
else this.diff(props)._start(onEnd)
129+
this._run(guid, props, didEnd)
149130
}
150131
})
151132
}
@@ -154,6 +135,16 @@ class Controller<DS extends object = any> {
154135
return this
155136
}
156137

138+
// Called by the frameloop
139+
onFrame(isActive: boolean) {
140+
if (this.props.onFrame) {
141+
this.props.onFrame(this.values)
142+
}
143+
if (!isActive) {
144+
this._stop(true)
145+
}
146+
}
147+
157148
stop(finished?: boolean) {
158149
if (!this.idle || this.pendingCount) {
159150
this.guid++
@@ -163,21 +154,52 @@ class Controller<DS extends object = any> {
163154
return this
164155
}
165156

166-
// Called by the frameloop
167-
onFrame(isActive: boolean) {
168-
if (this.props.onFrame) {
169-
this.props.onFrame(this.values)
157+
destroy() {
158+
this.stop()
159+
this.props = {} as any
160+
this.merged = {} as any
161+
this.values = {} as any
162+
this.interpolations = {}
163+
this.animations = {}
164+
this.configs = []
165+
}
166+
167+
// Add this controller to the frameloop
168+
private _start(onEnd?: OnEnd) {
169+
if (onEnd) this.onEndQueue.push(onEnd)
170+
if (this.idle) {
171+
this.idle = false
172+
start(this)
170173
}
171-
if (!isActive) {
172-
this._stop(true)
174+
}
175+
176+
private _stop(finished?: boolean) {
177+
this.idle = true
178+
stop(this)
179+
if (finished && this.props.onRest) {
180+
this.props.onRest(this.merged)
181+
}
182+
if (this.onEndQueue.length) {
183+
this.onEndQueue.forEach(onEnd => onEnd(finished))
184+
this.onEndQueue = []
185+
}
186+
}
187+
188+
private _run(guid: number, props: UpdateProps<DS>, onEnd?: OnEnd) {
189+
// Never call `onStart` for immediate animations
190+
if (!props.immediate) {
191+
const { onStart } = props
192+
// Allow `useCallback` to prevent multiple calls
193+
if (onStart && onStart !== this.props.onStart) onStart()
194+
}
195+
if (is.arr(props.to) || is.fun(props.to)) {
196+
this._runAsync(guid, props, onEnd)
197+
} else {
198+
this._diff(props)._start(onEnd)
173199
}
174200
}
175201

176-
runAsync(
177-
guid: number,
178-
props: SpringProps<DS>,
179-
onEnd?: (finished: boolean) => void
180-
) {
202+
private _runAsync(guid: number, props: UpdateProps<DS>, onEnd?: OnEnd) {
181203
// If "to" is either a function or an array it will be processed async, therefor "to" should be empty right now
182204
// If the view relies on certain values "from" has to be present
183205
let queue = Promise.resolve()
@@ -189,7 +211,7 @@ class Controller<DS extends object = any> {
189211
if (guid !== this.guid) return
190212
const fresh = { ...props, ...interpolateTo(p) }
191213
if (is.arr(fresh.config)) fresh.config = fresh.config[i]
192-
return new Promise<any>(r => this.diff(fresh).start(r))
214+
return new Promise<any>(r => this._diff(fresh)._start(r))
193215
})
194216
})
195217
} else if (is.fun(to)) {
@@ -202,7 +224,7 @@ class Controller<DS extends object = any> {
202224
if (guid !== this.guid) return
203225
const fresh = { ...props, ...interpolateTo(p) }
204226
if (is.arr(fresh.config)) fresh.config = fresh.config[i++]
205-
return (last = new Promise(r => this.diff(fresh).start(r)))
227+
return (last = new Promise(r => this._diff(fresh)._start(r)))
206228
},
207229
// cancel()
208230
finished => this.stop(finished)
@@ -219,11 +241,11 @@ class Controller<DS extends object = any> {
219241
})
220242
}
221243

222-
diff(props: any) {
244+
private _diff(props: UpdateProps<DS>) {
223245
this.props = { ...this.props, ...props }
224246
let {
225-
from = {},
226-
to = {},
247+
from = {} as any,
248+
to = {} as any,
227249
config = {},
228250
reverse,
229251
attach,
@@ -381,27 +403,17 @@ class Controller<DS extends object = any> {
381403
if (changed) {
382404
// Make animations available to frameloop
383405
this.configs = Object.values(this.animations)
384-
this.values = {} as ValuesFor<DS>
385-
this.interpolations = {} as InterpolationsFor<DS>
386-
for (let key in this.animations) {
387-
this.interpolations[key] = this.animations[key].interpolation
388-
this.values[key] = this.animations[key].interpolation.getValue()
406+
407+
this.interpolations = {}
408+
const values = (this.values = {} as any)
409+
for (const key in this.animations) {
410+
const { interpolation } = this.animations[key]
411+
this.interpolations[key] = interpolation
412+
values[key] = interpolation.getValue()
389413
}
390414
}
391415
return this
392416
}
393-
394-
destroy() {
395-
this.stop()
396-
this.props = {} as DS
397-
this.merged = {}
398-
this.animations = {} as AnimationsFor<DS>
399-
this.interpolations = {} as InterpolationsFor<DS>
400-
this.values = {} as ValuesFor<DS>
401-
this.configs = []
402-
}
403-
404-
getValues = () => this.interpolations
405417
}
406418

407419
export default Controller

types/renderprops-universal.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface SpringConfig {
2323
clamp?: boolean
2424
precision?: number
2525
delay?: number
26+
decay?: number
2627
duration?: number
2728
easing?: SpringEasingFunc
2829
}

0 commit comments

Comments
 (0)