requestAnimationFrame, кажется, обновляет переменные после вызова cancelAnimationFrame - PullRequest
0 голосов
/ 12 марта 2019

Я пытаюсь анимировать вращение камеры с помощью MapBoxGL, в то же время предоставляя возможность приостановить вращение и возобновить вращение с помощью флажка обратного вызова. Вращение «пауза» / «стоп» работает нормально, но «перезапуск», похоже, подхватывает анимацию в том месте, где она должна была быть, если она никогда не была приостановлена, в отличие от выбора места остановки анимации.

var animation;

function rotateCamera(timestamp) {
    map.rotateTo((timestamp / 600) % 360, {duration: 0});
    animation = requestAnimationFrame(rotateCamera);
}

Когда карта загружается, анимация вызывается с помощью:

animation = rotateCamera(0);

Обратный вызов выглядит так:

d3.selectAll("input[name='camerarotation-selection']").on("change", function() {
    if (d3.select("input[name='selection']").property("checked")) {
        rotateCamera(map.getBearing());
    } else {
        cancelAnimationFrame(animation);
    }
});

Если я console.log отметка времени var внутри функции rotateCamera, я вижу, что, несмотря на вызов cancelAnimationFrame, он продолжает увеличиваться. Я пытался объявить animation неопределенным при перезапуске, и это тоже не сработало. Я в тупике! Спасибо за вашу помощь.

1 Ответ

2 голосов
/ 12 марта 2019

Отметка времени, передаваемая обратному вызову requestAnimationFrame, представляет собой DOMHighResTimestamp, аналогичную отметке, возвращаемой performance.now(). Эта временная метка указывает количество миллисекунд, прошедших с начала времени жизни текущего документа (ну, может быть немного сложнее ), когда началось выполнение обратных вызовов.

Таким образом, даже если не выполняется цикл requestAnimationFrame , эта временная метка действительно увеличивается, как и Date.now().

let animation = 0;
inp.onchange = e => {
  if (inp.checked) start();
  else {
    cancelAnimationFrame(animation);
  }
};

function start(timestamp) {
  _log.textContent = timestamp;
  animation = requestAnimationFrame(start);
}

В вашем коде вы, вероятно, захотите сохранить только время, прошедшее с последнего кадра. Для этого вы можете просто сохранить timestamp в глобальной переменной, а затем в обратном вызове сделать

var elapsed = timestamp - last_frame;
last_frame = timestamp;

И не забудьте также позаботиться о случае возобновления, где timestamp будет неопределенным, а elapsed должно быть сброшено.



Теперь я хотел бы отметить, что ваше описание проблемы может также указывать на совершенно другую проблему: у вас может быть одновременно запущено более одного цикла.

Поскольку вы используете одну переменную animation для хранения frame_id (используется для cancelAnimationFrame), если вы вызовете rotateCamera, когда цикл уже запущен, первые frame_ids будут потеряны, а их цикл rAF действительно продолжить.

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();


 

Я думаю, что это возможно в вашем коде, поскольку input[name='camerarotation-selection'] может меняться несколько раз, когда input[name='selection'] не был изменен или даже если input[name='camerarotation-selection'] может быть несколькими элементами.

Чтобы избежать этого, вы можете сохранить переменную семафора, позволяющую вам знать, работает цикл или нет, и запускать его только тогда, когда он не работает. Или вы могли бы полностью избавиться от cancelAnimationFrame, используя только один семафор и выйдя из него в начале обратного вызова rAF:

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);
  }
}




...