diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index 03b7fba245..9f5311c1eb 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -1,6 +1,9 @@ export declare const animationFrame: AnimationFrameScheduler; -export declare function animationFrames(timestampProvider?: TimestampProvider): Observable; +export declare function animationFrames(timestampProvider?: TimestampProvider): Observable<{ + timestamp: number; + elapsed: number; +}>; export declare const animationFrameScheduler: AnimationFrameScheduler; diff --git a/spec-dtslint/observables/dom/animationFrames-spec.ts b/spec-dtslint/observables/dom/animationFrames-spec.ts index 249615ee0f..b7a6adc996 100644 --- a/spec-dtslint/observables/dom/animationFrames-spec.ts +++ b/spec-dtslint/observables/dom/animationFrames-spec.ts @@ -1,11 +1,11 @@ import { animationFrames } from 'rxjs'; it('should just be an observable of numbers', () => { - const o$ = animationFrames(); // $ExpectType Observable + const o$ = animationFrames(); // $ExpectType Observable<{ timestamp: number; elapsed: number; }> }); it('should allow the passing of a timestampProvider', () => { - const o$ = animationFrames(performance); // $ExpectType Observable + const o$ = animationFrames(performance); // $ExpectType Observable<{ timestamp: number; elapsed: number; }> }); it('should not allow the passing of an invalid timestamp provider', () => { diff --git a/spec/observables/dom/animationFrames-spec.ts b/spec/observables/dom/animationFrames-spec.ts index a6a6d78942..761510f198 100644 --- a/spec/observables/dom/animationFrames-spec.ts +++ b/spec/observables/dom/animationFrames-spec.ts @@ -27,9 +27,9 @@ describe('animationFrames', () => { const result = mapped.pipe(mergeMapTo(animationFrames())); expectObservable(result, subs).toBe(expected, { - a: ta - tm, - b: tb - tm, - c: tc - tm, + a: { elapsed: ta - tm, timestamp: ta }, + b: { elapsed: tb - tm, timestamp: tb }, + c: { elapsed: tc - tm, timestamp: tc }, }); }); }); @@ -50,9 +50,9 @@ describe('animationFrames', () => { const result = mapped.pipe(mergeMapTo(animationFrames(timestampProvider))); expectObservable(result, subs).toBe(expected, { - a: 50, - b: 150, - c: 250, + a: { elapsed: 50, timestamp: 100 }, + b: { elapsed: 150, timestamp: 200 }, + c: { elapsed: 250, timestamp: 300 }, }); }); }); @@ -71,8 +71,8 @@ describe('animationFrames', () => { const result = mapped.pipe(mergeMapTo(animationFrames().pipe(take(2)))); expectObservable(result).toBe(expected, { - a: ta - tm, - b: tb - tm, + a: { elapsed: ta - tm, timestamp: ta }, + b: { elapsed: tb - tm, timestamp: tb }, }); testScheduler.flush(); @@ -98,8 +98,8 @@ describe('animationFrames', () => { const result = mapped.pipe(mergeMapTo(animationFrames().pipe(takeUntil(signal)))); expectObservable(result).toBe(expected, { - a: ta - tm, - b: tb - tm, + a: { elapsed: ta - tm, timestamp: ta }, + b: { elapsed: tb - tm, timestamp: tb }, }); testScheduler.flush(); diff --git a/src/internal/observable/dom/animationFrames.ts b/src/internal/observable/dom/animationFrames.ts index ea45d25d15..6fcd2217ca 100644 --- a/src/internal/observable/dom/animationFrames.ts +++ b/src/internal/observable/dom/animationFrames.ts @@ -1,14 +1,14 @@ import { Observable } from '../../Observable'; import { Subscription } from '../../Subscription'; import { TimestampProvider } from "../../types"; -import { dateTimestampProvider } from '../../scheduler/dateTimestampProvider'; +import { performanceTimestampProvider } from '../../scheduler/performanceTimestampProvider'; import { requestAnimationFrameProvider } from '../../scheduler/requestAnimationFrameProvider'; /** * 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. + * Emits the 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 @@ -31,7 +31,7 @@ import { requestAnimationFrameProvider } from '../../scheduler/requestAnimationF * const diff = end - start; * return animationFrames().pipe( * // Figure out what percentage of time has passed - * map(elapsed => elapsed / duration), + * map(({elapsed}) => elapsed / duration), * // Take the vector while less than 100% * takeWhile(v => v < 1), * // Finish with 100% @@ -71,26 +71,45 @@ import { requestAnimationFrameProvider } from '../../scheduler/requestAnimationF * const source$ = animationFrames(customTSProvider); * * // Log increasing numbers 0...1...2... on every animation frame. - * source$.subscribe(x => console.log(x)); + * source$.subscribe(({ elapsed }) => console.log(elapsed)); * ``` * * @param timestampProvider An object with a `now` method that provides a numeric timestamp */ -export function animationFrames(timestampProvider: TimestampProvider = dateTimestampProvider) { - return timestampProvider === dateTimestampProvider ? DEFAULT_ANIMATION_FRAMES : animationFramesFactory(timestampProvider); +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) { +function animationFramesFactory(timestampProvider?: TimestampProvider) { const { schedule } = requestAnimationFrameProvider; - return new Observable(subscriber => { - const start = timestampProvider.now(); + return new Observable<{ timestamp: number, elapsed: number }>(subscriber => { let subscription: Subscription; - const run = () => { - subscriber.next(timestampProvider.now() - start); + // 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(); + const run = (timestamp: DOMHighResTimeStamp | number) => { + // 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 + }); if (!subscriber.closed) { subscription = schedule(run); } @@ -103,7 +122,7 @@ function animationFramesFactory(timestampProvider: TimestampProvider) { } /** - * In the common case, where `Date` is passed to `animationFrames` as the default, + * 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(dateTimestampProvider); +const DEFAULT_ANIMATION_FRAMES = animationFramesFactory();