[英]How to get exact scroll position when animating elements onScroll
我正在尝试根据元素在场景中的进度滚动时为元素设置动画。 我遇到了浏览器返回间歇滚动位置的问题,这使得在启动和停止元素动画时很难准确。
最小的例子:
小提琴小提琴演示
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 则非常有问题。 所以我想我会在这里发布这个问题,看看是否有人对这种情况有任何经验或见解。
任何指导将不胜感激,因为我已经考虑了一段时间。
让我们假设浏览器就是这样,并不是所有经过的像素都会导致事件。 您最终会得到远离结尾的元素,因为您会检查滚动位置是否“在场景中”。 当它之前是 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 没有类似的东西,所以你可以使用外部的东西
我同意artanik它在没有requestAnimationFrame
情况下工作得更好
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>
问题归结为 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() 。 如果您想重新使用它,请从这个工作版本向后工作,直到获得您想要实现的目标。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.