如何在动画元素 onScroll 时获得准确的滚动位置

How to get exact scroll position when animating elements onScroll

我正在尝试根据元素在场景中的进度滚动时为元素设置动画。我 运行 关注浏览器 return 间歇性滚动位置的问题,这使得在启动和停止元素动画时难以准确。

最小示例:

Fiddle Fiddle Demo

const tweens = document.querySelectorAll('.tween');
const trigger = document.querySelector('.start');
const triggerRect = trigger.getBoundingClientRect();

let yScroll = window.scrollY;
let yCurrent = yScroll;
let AF = null;

const start = triggerRect.top - yScroll - 200;
const end = start + 400;

const updateScroll = () => {
 yScroll = window.scrollY;
  startAnimation();
}

const startAnimation = () => {
  if(!AF) AF = requestAnimationFrame(animate)
}

const cancelAnimation = () => {
  yCurrent = yScroll;
  cancelAnimationFrame(AF);
  AF = null;
}

const animate = () => {
  if(yCurrent === yScroll) cancelAnimation();
  else {
   updateElements();
    yCurrent = yScroll;
    AF = requestAnimationFrame(animate);
  }
}

const updateElements = () => {
  const pos = yCurrent - start;
  if(inScene()){
    tweens[0].style.transform = `translateX(${pos}px)`;
    tweens[1].style.transform = `translateY(${pos}px)`;
  }
}

const inScene = () => {
  return start <= yCurrent && end >= yCurrent ? true : false;
};


window.addEventListener('scroll', updateScroll);
.wrapper{
  position:relative;
  margin: 100vh 20px;
  background: rgb(240,240,240);
  height: 900px;
  border: 1px solid red;
}

.line{
  position:absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background: red;
}

.start{
  top: 200px;
}

.end{
  bottom: 200px;
}

.tween{
  position:absolute;
  top:200px;
  width: 100px;
  height: 100px;
  background: blue;
}

.left{
  left: 0;
}

.right{
  right: 0;
}
<h1>Scroll Down</h1>
<div class="wrapper">
  <div class="line start trigger"></div>
  <div class="line end"></div>
  <div class="tween left"></div>
  <div class="tween right"></div>
</div>

如您所见,元素应该停在直线上,但当您滚动时,它们从来没有真正停止过。滚动得越快,情况就越糟糕。

那么有没有一种技术可以 return 正确的滚动位置或以某种方式对元素进行去抖动,以便在滚动时可以达到整个范围,而不是不断地达到预期位置?

我知道这是可能的,因为像 ScrollMagic 这样的滚动库可以很好地处理这个问题,但我真的不想解构整个 ScrollMagic 框架以了解它们是如何实现的。我会使用 ScrollMagic 框架本身,但我正在尝试使用动量样式的滚动容器来包装页面并将其与此容器内的元素一起滚动滚动并使用 ScrollMagic 它是非常有问题的。所以我想我会 post 这里的问题,看看是否有人对这种情况有任何经验或见解。

任何指导将不胜感激,因为我已经考虑了一段时间。

让我们假设浏览器就是这样,并不是所有经过的像素都会导致事件。你最终得到的元素远离末尾,因为你检查滚动位置是否为 "in scene"。当它是之前的 5 倍时 - 没关系。接下来是 5px 之后 - 你忽略它。您可以做的是,每当您滚动到场景外而不是忽略它时,将对象动画化到它们的最终位置。

所以只是为了开始它,我会改变这个:

const updateElements = () => {
  let pos = yCurrent - start;
  pos = Math.min(400, Math.max(0, pos));
  tweens[0].style.transform = `translateX(${pos}px)`;
  tweens[1].style.transform = `translateY(${pos}px)`;
}

它可以进一步优化,当它在 "scene" 之外时,它只会启动一次动画。如果您对此也有疑问,请告诉我。

更新:

这是仅在需要时才设置动画的版本:

const updateElements = () => {
  let pos = yCurrent - start;
  pos = Math.min(400, Math.max(0, pos));
  const styles = [...tweens].map(tween => window.getComputedStyle(tween, null));
  const transforms = styles.map(style => new WebKitCSSMatrix(style.getPropertyValue('transform')));
  if (transforms[0].m41 !== pos) {
    tweens[0].style.transform = `translateX(${pos}px)`;
  }

  if (transforms[1].m42 !== pos) {
    tweens[1].style.transform = `translateY(${pos}px)`;
  }
}

阅读 translate 是地狱,所以如果我们只关心 Webkit 浏览器,我们可以使用 WebKitCSSMatrix,但是 Firefox 没有这样的东西,所以你可以使用一些东西 external

我同意 artanik 没有 requestAnimationFrame

确实效果更好

问题在于 inScene() 变为 false 并且您没有将最终动画位置设置为最小/最大位置。这也是抽搐,因为 yScroll 值在动画开始之前不会更新,但为时已晚。

const updateElements = () => {
  yScroll = window.scrollY;
  var pos = yScroll - start;

  if (pos < 0) {
    pos = 0;
  }
  else if (pos > end - start) {
    pos = end - start;
  }


  //if (inScene()){
    tweens[0].style.transform = `translateX(${pos}px)`;
    tweens[1].style.transform = `translateY(${pos}px)`;
  //}
}

现在所有这些都有效,请参阅 jsfiddle:https://jsfiddle.net/nLh748y3/1/

我现在已经注释掉了 inScene()。如果你想要这个回来,那么从这个工作版本向后工作,直到你得到你想要实现的目标。

https://jsfiddle.net/b2pqndvh/

首先,如果没有足够的时间通过滚动来更新位置,则需要像 Math.min(END, Math.max(0, scrollY - start)); 一样设置“边界”。其次,我建议删除 requestAnimationFrame,因为此方法会告诉浏览器您希望在下一次重绘后执行更新。这就是为什么它看起来更笨重的原因。性能测试(在 chrome 和 firefox 中)表明在这个例子中没有 requestAnimationFrame 性能不会受到影响,但它可能会受到更复杂布局的影响。

const tweens = document.querySelectorAll('.tween');
const trigger = document.querySelector('.start');
const triggerRect = trigger.getBoundingClientRect();

const START = 0;
const MIDDLE = 200;
const END = 400;

const start = triggerRect.top - window.scrollY - MIDDLE;
const end = start + END;

const inSceneStart = scrollY => start <= scrollY;
const inSceneEnd = scrollY => end >= scrollY;

const updateTransform = value => {
  tweens[0].style.transform = `translateX(${value}px)`;
  tweens[1].style.transform = `translateY(${value}px)`;
}

const updateScroll = () => {
  const scrollY = window.scrollY;
  const pos = Math.min(END, Math.max(0, scrollY - start));
  updateTransform(pos);
}

window.addEventListener('scroll', updateScroll);
.wrapper{
  position:relative;
  margin: 100vh 20px;
  background: rgb(240,240,240);
  height: 900px;
}

.line{
  position:absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background: red;
}

.start{
  top: 200px;
}

.end{
  bottom: 200px;
}

.tween{
  position:absolute;
  top:200px;
  width: 100px;
  height: 100px;
  background: blue;
}

.left{
  left: 0;
}

.right{
  right: 0;
}
<h1>Scroll Down</h1>
<div class="wrapper">
  <div class="line start trigger"></div>
  <div class="line end"></div>
  <div class="tween left"></div>
  <div class="tween right"></div>
</div>