import { Observable } from '../../Observable'; | |
import { TimestampProvider } from '../../types'; | |
import { performanceTimestampProvider } from '../../scheduler/performanceTimestampProvider'; | |
import { animationFrameProvider } from '../../scheduler/animationFrameProvider'; | |
/** | |
* An observable of animation frames | |
* | |
* Emits the amount of time elapsed since subscription and the timestamp on each animation frame. | |
* Defaults to milliseconds provided to the requestAnimationFrame's callback. 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. | |
* | |
* ## Examples | |
* | |
* Tweening a div to move it on the screen | |
* | |
* ```ts | |
* import { animationFrames, map, takeWhile, endWith } from 'rxjs'; | |
* | |
* 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)`; | |
* }); | |
* ``` | |
* | |
* 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(({ elapsed }) => console.log(elapsed)); | |
* ``` | |
* | |
* @param timestampProvider An object with a `now` method that provides a numeric timestamp | |
*/ | |
export function animationFrames(timestampProvider?: TimestampProvider) { | |
return timestampProvider ? animationFramesFactory(timestampProvider) : DEFAULT_ANIMATION_FRAMES; | |
} | |
/** | |
* 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<{ timestamp: number; elapsed: number }>((subscriber) => { | |
// If no timestamp provider is specified, use performance.now() - as it | |
// will return timestamps 'compatible' with those passed to the run | |
// callback and won't be affected by NTP adjustments, etc. | |
const provider = timestampProvider || performanceTimestampProvider; | |
// Capture the start time upon subscription, as the run callback can remain | |
// queued for a considerable period of time and the elapsed time should | |
// represent the time elapsed since subscription - not the time since the | |
// first rendered animation frame. | |
const start = provider.now(); | |
let id = 0; | |
const run = () => { | |
if (!subscriber.closed) { | |
id = animationFrameProvider.requestAnimationFrame((timestamp: DOMHighResTimeStamp | number) => { | |
id = 0; | |
// Use the provider's timestamp to calculate the elapsed time. Note that | |
// this means - if the caller hasn't passed a provider - that | |
// performance.now() will be used instead of the timestamp that was | |
// passed to the run callback. The reason for this is that the timestamp | |
// passed to the callback can be earlier than the start time, as it | |
// represents the time at which the browser decided it would render any | |
// queued frames - and that time can be earlier the captured start time. | |
const now = provider.now(); | |
subscriber.next({ | |
timestamp: timestampProvider ? now : timestamp, | |
elapsed: now - start, | |
}); | |
run(); | |
}); | |
} | |
}; | |
run(); | |
return () => { | |
if (id) { | |
animationFrameProvider.cancelAnimationFrame(id); | |
} | |
}; | |
}); | |
} | |
/** | |
* In the common case, where the timestamp provided by the rAF API is used, | |
* we use this shared observable to reduce overhead. | |
*/ | |
const DEFAULT_ANIMATION_FRAMES = animationFramesFactory(); | |