'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 |