使用 requestAnimationFrame() 重启时如何保持箭头位置不变?

How to keep the arrow position unchanged when restart it using requestAnimationFrame()?

我试图理解 MDN 上的 example

current code的效果让我有点迷惑——暂停后如果稍等片刻再开始,箭头的位置不是原来停的地方。

如何确保从中断处重新开始?我想到了下面的方法,但是我不是很满意。

const spinner = document.querySelector('div')
let rotateCount = 0
let startTime = null
let rAF
let spinning = false
let previousRotateCount = 0
let hasStopped = false
let rotateInterval

function draw(timestamp) {
  if (hasStopped) {
    // The angle of each rotation is constant
    rotateCount += rotateInterval
    rotateCount %= 360
  } else {
    if (!startTime) {
      startTime = timestamp
    }
    rotateCount = (timestamp - startTime) / 3
    rotateCount %= 360
    rotateInterval = rotateCount - previousRotateCount
    previousRotateCount = rotateCount
  }
  console.log(rotateCount)
  spinner.style.transform = `rotate(${rotateCount}deg)`

  rAF = requestAnimationFrame(draw)
}

document.body.addEventListener('click', () => {
  if (spinning) {
    hasStopped = true
    cancelAnimationFrame(rAF)
  } else {
    draw()
  }
  spinning = !spinning
})
html {
  background-color: white;
  height: 100%;
}

body {
  height: inherit;
  background-color: #f00;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

div {
  display: inline-block;
  font-size: 10rem;
}
<div>↻</div>

希望有更好的方法。非常感谢。

这个示例代码有点奇怪,他们定义了一个从未实际使用过的全局 rotateCount 并且他们对时间戳的使用是......是的,令人困惑。

他们的 let rotateCount = (timestamp - startTime) / 3; 使得动画将花费 1080 毫秒来执行完整的旋转(360 度 x 3 = 1080 毫秒)。

但这是基于当前时间和开始时间的差异。所以确实,即使你暂停动画,当重新启动它时,它就像从未暂停过一样。

为此,您需要实际使用全局变量 rotateCount(不在 draw 中重新定义新变量),并每次按旋转量递增它自上次开奖以来需要,而不是从整体开始。

那么您只需要确保在恢复动画时更新最后绘制的时间戳,并让动画真正暂停和恢复。

const spinner = document.querySelector('div');

const duration = 1080; // ms to perform a full revolution
const speed = 360 / duration;

let rotateCount = 0;
let rAF;

// we'll set this in the starting code
// (in the click event listener)
let lastTime;

let spinning = false;

// Create a draw() function
function draw(timestamp) {
  // get the elapsed time since the last time we did fire
  // we directly get the remainder from "duration"
  // because we're in a looping animation
  const elapsed = (timestamp - lastTime) % duration;
  // for next time, lastTime is now
  lastTime = timestamp;
  // add to the previous rotation how much we did rotate
  rotateCount += elapsed * speed;
  
  // Set the rotation of the div to be equal to rotateCount degrees
  spinner.style.transform = 'rotate(' + rotateCount + 'deg)';

  // Call the next frame in the animation
  rAF = requestAnimationFrame(draw);
}

// event listener to start and stop spinner when page is clicked

document.body.addEventListener('click', () => {
  if(spinning) {
    cancelAnimationFrame(rAF);
    spinning = false;
  } else {
    // reset lastTime with either the current frame time
    // or JS time if unavailable
    // so that in the next draw
    // we see only the time that did elapse
    // from now to then
    lastTime = document.timeline?.currentTime || performance.now();
    // schedule the next draw
    requestAnimationFrame( draw )
    spinning = true;
  }
});
html {
  background-color: white;
  height: 100%;
}

body {
  height: inherit;
  background-color: red;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

div {
  display: inline-block;
  font-size: 10rem;
  user-select: none;
}
<div>↻</div>