'How can I use RxJS to generate a requestAnimationFrame loop?

My goal is to create an animation loop à la requestAnimationFrame so that I could do something like this:

animationObservable.subscribe(() =>
{
    // drawing code here
});

I tried this code as a basic test:

let x = 0;

Rx.Observable
    .of(0)
    .repeat(Rx.Scheduler.animationFrame)
    .takeUntil(Rx.Observable.timer(1000))
    .subscribe(() => console.log(x++));

Here is a JSFiddle but I'm not liable for any browser crashes from running this.

I expected this to log the numbers from 0 to approximately 60 (because that is my monitor's refresh rate) over 1 second. Instead, it rapidly logs numbers (much faster than requestAnimationFrame would), begins to cause the page to lag, and finally overflows the stack around 10000 and several seconds later.

Why does the animationFrame scheduler behave this way, and what is the correct way to run an animation loop with RxJS?



Solution 1:[1]

It's because the default behaviour of Observable.of is to emit immediately.

To change this behaviour, you should specify the Scheduler when calling Observable.of:

let x = 0;

Rx.Observable
    .of(0, Rx.Scheduler.animationFrame)
    .repeat()
    .takeUntil(Rx.Observable.timer(1000))
    .subscribe(() => console.log(x++));
<script src="https://npmcdn.com/@reactivex/[email protected]/dist/global/Rx.min.js"></script>

Or, more simply, replace the of and repeat operators with:

Observable.interval(0, Rx.Scheduler.animationFrame)

Solution 2:[2]

This is how I use requestAnimationFrame with rxjs. I've seen a lot of developers using 0 instead of animationFrame.now(). It's much better to pass the time because you often need that in animations.

const { Observable, Scheduler } = Rx;

const requestAnimationFrame$ = Observable
  .defer(() => Observable
    .of(Scheduler.animationFrame.now(), Scheduler.animationFrame)
    .repeat()
    .map(start => Scheduler.animationFrame.now() - start)
  );

// Example usage
const duration$ = duration => requestAnimationFrame$
  .map(time => time / duration)
  .takeWhile(progress => progress < 1)
  .concat([1])

duration$(60000)
  .subscribe((i) => {
    clockPointer.style.transform = `rotate(${i * 360}deg)`;
  });
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>

<div style="border: 3px solid black; border-radius: 50%; width: 150px; height: 150px;">
  <div id="clockPointer" style="width: 2px; height: 50%; background: black; margin-left: 50%; padding-left: -1px; transform-origin: 50% 100%;"></div>
</div>

Solution 3:[3]

As of RxJs >= 5.5 you do it the following way:

import { animationFrameScheduler, of, timer } from 'rxjs';
import { repeat,takeUntil } from 'rxjs/operators';

let x = 0;

    of(null, animationFrameScheduler)
      .pipe(
        repeat(),
        takeUntil(timer(1000)),
      )
      .subscribe(() => {
        console.log(x++);
      });

OR:

import { animationFrameScheduler, of, timer } from 'rxjs';
import { repeat, tap, takeUntil } from 'rxjs/operators';

let x = 0;

of(null, animationFrameScheduler)
  .pipe(
    tap(() => {
      console.log(x++);
    }),
    repeat(),
    takeUntil(timer(1000)),
  )
  .subscribe();

Solution 4:[4]

From Ben Lesh's presentation about animation in 2017 and adjusted to RxJS 7, plus my small tweak to add time difference from last frame:

let animationFrameObservable = rxjs.defer(() => {
    // using defer so startTime is set on subscribe, not now
    const startTime = rxjs.animationFrameScheduler.now();
    let lastTime = startTime;
    // using interval but not using the default scheduler
    return rxjs.interval(0, rxjs.animationFrameScheduler).pipe(
        // modify output to return elapsed time and time diff from last frame
        rxjs.map(() => {
            const currentTime = rxjs.animationFrameScheduler.now();
            const diff = currentTime - lastTime;
            lastTime = currentTime;
            return [currentTime - startTime, diff];
        }));
});

// use the observable
animationFrameObservable.pipe(
    rxjs.tap(x => console.log(x)),
    rxjs.takeUntil(rxjs.timer(10000))
).subscribe();

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Mick
Solution 4 Endy Tjahjono