Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 696
Kendall Frey Avatar asked Jan 24 '17 01:01

Kendall Frey


3 Answers

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)
like image 189
cartant Avatar answered Oct 17 '22 15:10

cartant


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>
like image 32
Olle Bröms Avatar answered Oct 17 '22 15:10

Olle Bröms


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();
like image 4
Mick Avatar answered Oct 17 '22 15:10

Mick