Как получить точную позицию прокрутки при анимации элементов на Scroll - PullRequest
2 голосов
/ 14 февраля 2020

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

Минимальный пример:

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>

Как вы можете видеть, элементы должны останавливаться на линиях, но когда вы прокручиваете, они на самом деле никогда не делают. И чем быстрее вы прокручиваете, тем хуже становится.

Таким образом, есть метод, который либо возвращает правильную позицию прокрутки, либо каким-то образом отбрасывает элементы, чтобы затем достигать полного диапазона при прокрутке, а не постоянно подниматься не хватает их предполагаемой позиции?

Я знаю, что это возможно, потому что библиотеки прокрутки, такие как ScrollMagi c, справляются с этим довольно хорошо, но я не хочу разбирать тогда весь ScrollMagi c рамки, чтобы узнать, как они этого достигают. Я бы использовал сам фреймворк ScrollMagi c, но я стараюсь, чтобы контейнер прокрутки в стиле импульса оборачивал страницу и переводил ее по прокрутке вместе с элементами внутри этого контейнера, а с помощью ScrollMagi c он довольно глючный. Поэтому я решил опубликовать вопрос здесь и посмотреть, есть ли у кого-нибудь опыт или понимание ситуации.

Буду признателен за любые рекомендации, так как я обдумывал это некоторое время.

Ответы [ 3 ]

1 голос
/ 17 февраля 2020

https://jsfiddle.net/b2pqndvh/

Во-первых, вам нужно установить «границы», например Math.min(END, Math.max(0, scrollY - start));, если не хватает времени для обновления позиции с помощью прокрутки. Во-вторых, я бы порекомендовал удалить requestAnimationFrame, поскольку этот метод сообщает браузеру, что вы sh выполните обновление после следующей перерисовки. Вот почему это выглядит более неуклюже с этим. Тесты производительности (в 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>
1 голос
/ 16 февраля 2020

Давайте предположим, что браузер - это то, чем он является, и не все пройденные пиксели приведут к событию. Вы заканчиваете с элементами далеко от конца, потому что вы проверяете положение прокрутки, если оно «в сцене». Когда до этого 5x - хорошо. Следующая вещь это 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 - ад * И я согласен с artanik он работает лучше без requestAnimationFrame

0 голосов
/ 16 февраля 2020

Проблема заключается в том, что inScene () становится ложным, и вы не устанавливаете конечную позицию анимации на минимальную / максимальную позицию. Это также было рывком, потому что значение 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 () на данный момент. Если вы хотите вернуть это обратно, работайте в обратном направлении с этой рабочей версии, пока не получите то, чего хотите достичь.

...