I'm trying to animate a camera rotation using MapBoxGL, while providing the option to pause the rotation and restart the rotation with a checkbox callback. The 'pause'/'stop' rotation works fine, but the 'restart' seems to pick up the animation where the it should have been if it was never paused, as opposed to picking up where the animation stopped.
var animation;
function rotateCamera(timestamp) {
map.rotateTo((timestamp / 600) % 360, {duration: 0});
animation = requestAnimationFrame(rotateCamera);
}
When the map loads, the animation is called with:
animation = rotateCamera(0);
The callback looks like this:
d3.selectAll("input[name='camerarotation-selection']").on("change", function() {
if (d3.select("input[name='selection']").property("checked")) {
rotateCamera(map.getBearing());
} else {
cancelAnimationFrame(animation);
}
});
If I console.log
the timestamp var inside the rotateCamera
function, I can see that despite the cancelAnimationFrame
call, it continues to be incremented. I have tried declaring animation
to be undefined upon a restart, and that doesn't seem to work either. I'm stumped! Thanks for your help.
The timestamp passed to the callback of requestAnimationFrame
is an DOMHighResTimestamp, similar to the one returned by performance.now()
.
This timestamp indicates the number of milliseconds that elapsed since the beginning of the current document's lifetime (well it can be a bit more complicated) when the callbacks execution started.
So even when no requestAnimationFrame loop is running, this timestamp indeed increments, just like Date.now()
also does.
let animation = 0;
inp.onchange = e => {
if (inp.checked) start();
else {
cancelAnimationFrame(animation);
}
};
function start(timestamp) {
_log.textContent = timestamp;
animation = requestAnimationFrame(start);
}
<input type="checkbox" id="inp">
<pre id="_log"></pre>
In your code, you will probably want to only keep the time that elapsed since last frame. To do so, you can simply save timestamp
in a variable that is globally available, and then in your callback do
var elapsed = timestamp - last_frame;
last_frame = timestamp;
And remember to also take care of the resume case, where timestamp
will be undefined and elapsed
should be reset.
Now, I'd like to point out that your description of the problem could also indicate an other problem entirely: you could have more than a single loop running simultaneously.
Since you are using a single animation
variable to hold the frame_id (used by cancelAnimationFrame), if you do call rotateCamera
while a loop is already running, the first frame_ids will get lost, and their rAF loop will indeed continue.
let common_id = 0; // OP's `animation` variable
let first_id = 0;
let second_id = 0;
function loop1(timestamp) {
common_id = first_id = requestAnimationFrame(loop1);
log_1.textContent = "loop 1: " + timestamp;
}
function loop2(timestamp) {
common_id = second_id = requestAnimationFrame(loop2);
log_2.textContent = "loop 2: " + timestamp;
}
btn.onclick = e => {
console.log("first loop's id", first_id);
console.log("second loop's id", second_id);
console.log('clearing common_id', common_id);
cancelAnimationFrame(common_id);
}
loop1();
loop2();
<button id="btn">stop the loop</button>
<pre id="log_1"></pre>
<pre id="log_2"></pre>
I think it is possible in your code, since input[name='camerarotation-selection']
could change multiple times, when input[name='selection']
had not chnaged, or even since input[name='camerarotation-selection']
could be multiple elements.
To avoid that, you could keep a semaphore variable allowing you to know if the loop is running or not, and to only start it when it's not.
Or you could even get rid entirely of cancelAnimationFrame
by using only one semaphore, and exiting early in the rAF callback:
let stopped = true;
function loop(timestamp) {
if (stopped) return; // exit early
requestAnimationFrame(loop);
log.textContent = timestamp;
}
// you can click it several times
btn_start.onclick = e => {
if (stopped === true) { // only if not running yet
stopped = false;
requestAnimationFrame(loop);
}
}
btn_stop.onclick = e => {
stopped = true; // deal only with the semaphore
};
btn_switch.onclick = e => {
stopped = !stopped;
if (stopped === false) { // we were paused
// start again
requestAnimationFrame(loop);
}
}
<button id="btn_start">start the loop</button>
<button id="btn_stop">stop the loop</button>
<button id="btn_switch">switch the loop</button>
<pre id="log"></pre>