Skip to content

Commit

Permalink
fix(animationFrames): emit the timestamp from the rAF's callback (#5438)
Browse files Browse the repository at this point in the history
* fix(animationFrames): emit the timestamp from the rAF's callback

Emit the timestamp provided by rAF to align better with the rAF
specification. Multiple scheduled rAF callbacks within the same frame
will be called with the same timestamp.

* fix(animationFrames): use shared observable for common use case

* feat(animationFrames): provide timestamp and elapsed time

* fix(animationFrames): fix type tests

* fix: base elapsed on subscription time

And use performance.now() for the elapsed calculation instead of the
passed timestamp - see comment for explanation.

* chore: update API guardian

* chore: update animate-based tests

Co-authored-by: Nicholas Jamieson <nicholas@cartant.com>
  • Loading branch information
tmair and cartant authored Jul 31, 2020
1 parent edd6731 commit c980ae6
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 27 deletions.
5 changes: 4 additions & 1 deletion api_guard/dist/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export declare const animationFrame: AnimationFrameScheduler;

export declare function animationFrames(timestampProvider?: TimestampProvider): Observable<number>;
export declare function animationFrames(timestampProvider?: TimestampProvider): Observable<{
timestamp: number;
elapsed: number;
}>;

export declare const animationFrameScheduler: AnimationFrameScheduler;

Expand Down
4 changes: 2 additions & 2 deletions spec-dtslint/observables/dom/animationFrames-spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { animationFrames } from 'rxjs';

it('should just be an observable of numbers', () => {
const o$ = animationFrames(); // $ExpectType Observable<number>
const o$ = animationFrames(); // $ExpectType Observable<{ timestamp: number; elapsed: number; }>
});

it('should allow the passing of a timestampProvider', () => {
const o$ = animationFrames(performance); // $ExpectType Observable<number>
const o$ = animationFrames(performance); // $ExpectType Observable<{ timestamp: number; elapsed: number; }>
});

it('should not allow the passing of an invalid timestamp provider', () => {
Expand Down
20 changes: 10 additions & 10 deletions spec/observables/dom/animationFrames-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
});
});
Expand All @@ -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 },
});
});
});
Expand All @@ -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();
Expand All @@ -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();
Expand Down
47 changes: 33 additions & 14 deletions src/internal/observable/dom/animationFrames.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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%
Expand Down Expand Up @@ -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<number>(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);
}
Expand All @@ -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();

0 comments on commit c980ae6

Please sign in to comment.