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();