-
Notifications
You must be signed in to change notification settings - Fork 3k
/
animationFrames.ts
107 lines (103 loc) · 3.46 KB
/
animationFrames.ts
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
import { Observable } from '../../Observable';
// TODO: move to types.ts
export interface TimestampProvider {
now(): number;
}
/**
* An observable of animation frames
*
* Emits the the amount of time elapsed since subscription on each animation frame. Defaults to elapsed
* milliseconds. Does not end on its own.
*
* Every subscription will start a separate animation loop. Since animation frames are always scheduled
* by the browser to occur directly before a repaint, scheduling more than one animation frame synchronously
* should not be much different or have more overhead than looping over an array of events during
* a single animation frame. However, if for some reason the developer would like to ensure the
* execution of animation-related handlers are all executed during the same task by the engine,
* the `share` operator can be used.
*
* This is useful for setting up animations with RxJS.
*
* ### Example
*
* Tweening a div to move it on the screen
*
* ```ts
* import { animationFrames } from 'rxjs';
* import { map, takeWhile, endWith } from 'rxjs/operators';
*
* function tween(start: number, end: number, duration: number) {
* const diff = end - start;
* return animationFrames().pipe(
* // Figure out what percentage of time has passed
* map(elapsed => elapsed / duration),
* // Take the vector while less than 100%
* takeWhile(v => v < 1),
* // Finish with 100%
* endWith(1),
* // Calculate the distance traveled between start and end
* map(v => v * diff + start)
* );
* }
*
* // Setup a div for us to move around
* const div = document.createElement('div');
* document.body.appendChild(div);
* div.style.position = 'absolute';
* div.style.width = '40px';
* div.style.height = '40px';
* div.style.backgroundColor = 'lime';
* div.style.transform = 'translate3d(10px, 0, 0)';
*
* tween(10, 200, 4000).subscribe(x => {
* div.style.transform = `translate3d(${x}px, 0, 0)`;
* });
* ```
*
* ### Example
*
* Providing a custom timestamp provider
*
* ```ts
* import { animationFrames, TimestampProvider } from 'rxjs';
*
* // A custom timestamp provider
* let now = 0;
* const customTSProvider: TimestampProvider = {
* now() { return now++; }
* };
*
* const source$ = animationFrames(customTSProvider);
*
* // Log increasing numbers 0...1...2... on every animation frame.
* source$.subscribe(x => console.log(x));
* ```
*
* @param timestampProvider An object with a `now` method that provides a numeric timestamp
*/
export function animationFrames(timestampProvider: TimestampProvider = Date) {
return timestampProvider === Date ? DEFAULT_ANIMATION_FRAMES : animationFramesFactory(timestampProvider);
}
/**
* Does the work of creating the observable for `animationFrames`.
* @param timestampProvider The timestamp provider to use to create the observable
*/
function animationFramesFactory(timestampProvider: TimestampProvider) {
return new Observable<number>(subscriber => {
let id: number;
const start = timestampProvider.now();
const run = () => {
subscriber.next(timestampProvider.now() - start);
if (!subscriber.closed) {
id = requestAnimationFrame(run);
}
};
id = requestAnimationFrame(run);
return () => cancelAnimationFrame(id);
});
}
/**
* In the common case, where `Date` is passed to `animationFrames` as the default,
* we use this shared observable to reduce overhead.
*/
const DEFAULT_ANIMATION_FRAMES = animationFramesFactory(Date);