【问题标题】:How to get exact scroll position when animating elements onScroll动画元素onScroll时如何获得精确的滚动位置
【发布时间】:2020-02-14 18:24:20
【问题描述】:

我正在尝试根据元素在场景中的滚动来为元素设置动画。我遇到了浏览器返回间歇性滚动位置的问题,这使得在启动和停止元素动画时很难准确。

小例子:

小提琴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>

如您所见,元素应该停止在行上,但是当您滚动时,它们实际上并没有。而且你滚动得越快,它就会变得越糟糕。

那么是否有一种技术可以返回正确的滚动位置或以某种方式使元素去抖动,以便在滚动时可以达到整个范围,而不是不断地低于预期位置?

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

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

【问题讨论】:

  • 我曾经遇到过 Intersection Observer 但没有时间实现它,我认为它可以提供帮助但你必须自己做,或者找到一些帮助库,这里是开始的参考如果你没有听说过他们developer.mozilla.org/en-US/docs/Web/API/…
  • @LouayAlosh 我对交叉点观察者的问题是我想创建一个基于进度的动画。我不知道交叉点观察者是否有可能获得元素在场景中取得的进展。因此,如果我想将元素设置为从开始到结束移动 200px。使用交点观察者,它只检查元素是否与顶部或底部相交。据我所知,没有办法检查交叉点之间的进度。回调仅在元素相交时触发。
  • 你是对的,对不起,我认为它可以提供帮助。
  • 您可能希望hook into the scroll event 改为passive event listener(这样浏览器就可以向您发送高保真事件更新,并且不会断断续续地滚动内容)跨度>

标签: javascript html css css-animations onscroll


【解决方案1】:

让我们假设浏览器就是这样,并不是所有经过的像素都会导致一个事件。您最终会得到远离末端的元素,因为您检查滚动位置是否“在场景中”。当它是之前的 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)`;
}

它可以进一步优化,因此它只会在“场景”之外启动一次动画。如果您对此也有疑问,请告诉我。

更新:

这是仅在需要时才进行动画处理的版本:

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,效果会更好

【讨论】:

    【解决方案2】:

    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>

    【讨论】:

    • requestAnimationFrame 的回调在重绘之前调用,而不是之后。事实上,与 setTimeout 或其他方法不同,它在每次绘制之前始终被调用一次,并且在绘制之前 使其成为动画的理想选择。然而,在以前的 Safari 和 Edge 版本中,requestAnimationFrame 的 cb 在绘制后被错误地调用,这与标准 bugs.webkit.org/show_bug.cgi?id=177484 相矛盾。
    【解决方案3】:

    问题归结为 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()。如果您想恢复它,请从这个工作版本向后工作,直到您获得想要实现的目标。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-07-28
      • 2019-11-19
      • 1970-01-01
      • 2022-11-23
      • 2014-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多