-
Notifications
You must be signed in to change notification settings - Fork 70
/
Copy pathindex.js
168 lines (145 loc) · 4.82 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import easingTypes, {easeInOutQuad} from 'tween-functions';
import requestAnimationFrame from 'raf';
// additive is the new iOS 8 default. In most cases it simulates a physics-
// looking overshoot behavior (especially with easeInOut. You can test that in
// the example
const DEFAULT_STACK_BEHAVIOR = 'ADDITIVE';
const DEFAULT_EASING = easeInOutQuad;
const DEFAULT_DURATION = 300;
const DEFAULT_DELAY = 0;
const stackBehavior = {
ADDITIVE: 'ADDITIVE',
DESTRUCTIVE: 'DESTRUCTIVE',
};
const Mixin = {
_rafID: null,
getInitialState() {
return {
tweenQueue: [],
};
},
componentWillUnmount() {
requestAnimationFrame.cancel(this._rafID);
this._rafID = -1;
},
tweenState(path, {easing, duration, delay, beginValue, endValue, onEnd, stackBehavior: configSB}) {
this.setState(state => {
let cursor = state;
let stateName;
// see comment below on pash hash
let pathHash;
if (typeof path === 'string') {
stateName = path;
pathHash = path;
} else {
for (let i = 0; i < path.length - 1; i++) {
cursor = cursor[path[i]];
}
stateName = path[path.length - 1];
pathHash = path.join('|');
}
// see the reasoning for these defaults at the top of file
const newConfig = {
easing: easing || DEFAULT_EASING,
duration: duration == null ? DEFAULT_DURATION : duration,
delay: delay == null ? DEFAULT_DELAY : delay,
beginValue: beginValue == null ? cursor[stateName] : beginValue,
endValue: endValue,
onEnd: onEnd,
stackBehavior: configSB || DEFAULT_STACK_BEHAVIOR,
};
let newTweenQueue = state.tweenQueue;
if (newConfig.stackBehavior === stackBehavior.DESTRUCTIVE) {
newTweenQueue = state.tweenQueue.filter(item => item.pathHash !== pathHash);
}
// we store path hash, so that during value retrieval we can use hash
// comparison to find the path. See the kind of shitty thing you have to
// do when you don't have value comparison for collections?
newTweenQueue.push({
pathHash: pathHash,
config: newConfig,
initTime: Date.now() + newConfig.delay,
});
// sorry for mutating. For perf reasons we don't want to deep clone.
// guys, can we please all start using persistent collections so that
// we can stop worrying about nonesense like this
cursor[stateName] = newConfig.endValue;
if (newTweenQueue.length === 1) {
this._rafID = requestAnimationFrame(this._rafCb.bind(this));
}
// this will also include the above mutated update
return {tweenQueue: newTweenQueue};
});
},
getTweeningValue(path) {
const state = this.state;
let tweeningValue;
let pathHash;
if (typeof path === 'string') {
tweeningValue = state[path];
pathHash = path;
} else {
tweeningValue = state;
for (let i = 0; i < path.length; i++) {
tweeningValue = tweeningValue[path[i]];
}
pathHash = path.join('|');
}
let now = Date.now();
for (let i = 0; i < state.tweenQueue.length; i++) {
const {pathHash: itemPathHash, initTime, config} = state.tweenQueue[i];
if (itemPathHash !== pathHash) {
continue;
}
const progressTime = now - initTime > config.duration
? config.duration
: Math.max(0, now - initTime);
// `now - initTime` can be negative if initTime is scheduled in the
// future by a delay. In this case we take 0
// if duration is 0, consider that as jumping to endValue directly. This
// is needed because the easing functino might have undefined behavior for
// duration = 0
const easeValue = config.duration === 0 ? config.endValue : config.easing(
progressTime,
config.beginValue,
config.endValue,
config.duration,
// TODO: some funcs accept a 5th param
);
const contrib = easeValue - config.endValue;
tweeningValue += contrib;
}
return tweeningValue;
},
_rafCb() {
const state = this.state;
if (state.tweenQueue.length === 0) {
return;
}
const now = Date.now();
let newTweenQueue = [];
for (let i = 0; i < state.tweenQueue.length; i++) {
const item = state.tweenQueue[i];
const {initTime, config} = item;
if (now - initTime < config.duration) {
newTweenQueue.push(item);
} else {
config.onEnd && config.onEnd();
}
}
// onEnd might trigger a parent callback that removes this component
// -1 means we've canceled it in componentWillUnmount
if (this._rafID === -1) {
return;
}
this.setState({
tweenQueue: newTweenQueue,
});
this._rafID = requestAnimationFrame(this._rafCb.bind(this));
},
};
export default {
Mixin,
easingTypes,
stackBehavior,
};