Плавный переход, когда позиция не является целым числом - PullRequest
3 голосов
/ 25 февраля 2020

Я пытаюсь отобразить какую-то временную шкалу, и моя цель - плавно обновить ее.

Мне удалось получить что-то лучше, чем абсолютное позиционирование, используя свойство css transform, но я не очень счастлив, потому что есть некоторое мерцание (особенно, когда фон темный).

var background = document.querySelector('#background');
var position = document.querySelector('#position');
var transform = document.querySelector('#transform');

var backgroundColor, borderColor

background.addEventListener('change', e => {

	backgroundColor = e.target.checked ? '#333333' : 'white'
  position.style.backgroundColor = backgroundColor
  transform.style.backgroundColor = backgroundColor

});

let current = 0
let step = 30
for (let i = 0; i < 300; i++) {
	for (let j = 0; j < 2; j++) {
    var element = document.createElement('div')
    element.style.position = 'absolute'
    element.style.height = '50px'
    element.style.width = step + 'px'
    if(j) {
      element.style.left = current + 'px'
    } else {
      element.style.left = 0 + 'px'
      element.style.transform = 'translateX(' + current + 'px)'
    }
    element.style['border-left'] = '1px gray solid'
    if (j)
      position.appendChild(element)
    else
      transform.appendChild(element)
  }
  
  current += step
}

setInterval(refresh, 50);

let init = 0
function refresh() {
	init -= 0.2
	let current = init
  for (var i = 0; i < position.children.length; i++) {
    var c = position.children[i];
  	c.style.left = current + 'px'
  	current += step
  }
  current = init
  for (var i = 0; i < transform.children.length; i++) {
    var c = transform.children[i];
  	c.style.transform = 'translateX(' + current + 'px)'
  	current += step
  }
}
<html>
<body>
<div>
  <input type="checkbox" id="background" label="Dark"> 
  <label>Dark</label>
</div>
<span>Using css position<span>
<div id="position" style="width:100%;height:50px;margin-bottom:1em;"></div>

<span>Using css transform<span>
<div id="transform" style="width:100%;height:50px;"></div>
</body>
</html>

Есть ли лучший способ сделать это?

Ответы [ 2 ]

3 голосов
/ 27 февраля 2020

Вы можете попробовать использовать translate3d, который теоретически будет лучше работать с долями пикселей (обрабатывается графическим процессором). Тем не менее, он может немного размыть, когда на долю пикселей.

Я думаю, что вы также можете получить лучшие результаты, если вы используете requestAnimationFrame вместо setInterval. Использование requestAnimationFrame, вероятно, потребует установки какого-то таймера для вычисления количества анимации (поскольку вы не знаете, сколько времени прошло между каждым вызовом)

Ниже приведен пример translate3d, хотя я думаю, что разные браузеры реализовать это по-другому, так что это может или не может привести к лучшим результатам.

РЕДАКТИРОВАТЬ
Я добавил пример requestAnimationFrame, может быть, я не выделил его достаточно изначально, чтобы он использовался вместо setInterval. Есть много ресурсов на сайтах о том, почему это так.

Я также добавил пример простой анимации div-оболочки, а не 300 строк, так как 1 анимация, а не 300, очевидно, даст большую производительность. Почему ОП не делает это таким образом, я не уверен, возможно, есть веская причина.

Несмотря на то, что вы можете не видеть большой разницы на вашей машине, на более медленной машине эта разница будет более заметной.

Если вы хотите глубже погрузиться в анимацию, вот отличное видео Пола Льюиса на эту тему:
https://www.youtube.com/watch?v=ohc8ejzSn48

var background = document.querySelector('#background');
var position = document.querySelector('#position');
var transform = document.querySelector('#transform');
var translate = document.querySelector('#translate');
var animFrame = document.querySelector('#animFrame');
var animWrapOuter = document.querySelector('#animWrapOuter');
var animWrapper = document.querySelector('#animWrapper');

var backgroundColor, borderColor

background.addEventListener('change', e => {

	backgroundColor = e.target.checked ? '#333333' : 'white'
  position.style.backgroundColor = backgroundColor
  transform.style.backgroundColor = backgroundColor
  translate.style.backgroundColor = backgroundColor
  animFrame.style.backgroundColor = backgroundColor
  animWrapOuter.style.backgroundColor = backgroundColor

});

let current = 0
let step = 30
for (let i = 0; i < 300; i++) {
	for (let j = 0; j < 5; j++) {
    var element = document.createElement('div')
    element.style.position = 'absolute'
    element.style.height = '50px'
    element.style.width = step + 'px'
    if(j == 0) {
      element.style.left = current + 'px'
    } else if(j == 1) {
      element.style.left = 0 + 'px'
      element.style.transform = 'translateX(' + current + 'px)'
    } else if(j == 2) {
    	element.style.left = 0 + 'px'
      element.style.transform = 'translate3d(' + current + 'px, 0px, 0px)'
    } else if(j == 3) {
    	element.style.left = 0 + 'px'
      element.style.transform = 'translate3d(' + current + 'px, 0px, 0px)'
    } else if(j == 4) {
    	element.style.left = 0 + 'px'
      element.style.transform = 'translate3d(' + current + 'px, 0px, 0px)'
    }
    
    element.style['border-left'] = '1px gray solid'
    if (j == 0) {
      position.appendChild(element)
    } else if(j == 1) {
      transform.appendChild(element)
    } else if(j == 2) {
    	translate.appendChild(element)
    } else if(j == 3) {
    	animFrame.appendChild(element)
    } else if(j == 4) {
    	animWrapper.appendChild(element)
    }
  }
  
  current += step
}

setInterval(refresh, 50);

let init = 0
function refresh() {
	init -= 0.2
	let current = init
  for (var i = 0; i < position.children.length; i++) {
    var c = position.children[i];
  	c.style.left = current + 'px'
  	current += step
  }
  current = init
  for (var i = 0; i < transform.children.length; i++) {
    var c = transform.children[i];
  	c.style.transform = 'translateX(' + current + 'px)'
  	current += step
  }
  current = init
  for (var i = 0; i < translate.children.length; i++) {
    var c = translate.children[i];
  	c.style.transform = 'translate3d(' + current + 'px, 0px, 0px)'
  	current += step
  }
}

// Set a speed value.
let speed = -0.004;

function animLoop() {
	let then = Date.now();
  let current = 0;
	function loop() {
  	requestAnimationFrame(loop);
    let now = Date.now();
    // Get the difference between now and the last time the loop ran.
    let delta = now - then;
    // Set the time when this loop ran.
    then = Date.now();
    current = current + (delta * speed);
    
    // Animate the wrapper (5th example)
    animWrapper.style.transform = 'translate3d(' + current + 'px, 0px, 0px)';
    // loop over and animate translate3d requestAnimationFrame children (4th example).
    for (var i = 0; i < animFrame.children.length; i++) {
    	var c = animFrame.children[i];
      c.style.transform = 'translate3d(' + (current + (step * i)) + 'px, 0px, 0px)';
    }
  }
  loop();
}
animLoop();
<div>
  <input type="checkbox" id="background" label="Dark"> 
  <label>Dark</label>
</div>
<span>Using css position</span>
<div id="position" style="width:100%;height:50px;margin-bottom:1em;"></div>

<span>Using css transform</span>
<div id="transform" style="width:100%;height:50px;"></div>

<span>Using css translate3d</span>
<div id="translate" style="width:100%;height:50px;"></div>

<span>Using css translate3d and requestAnimationFrame()</span>
<div id="animFrame" style="width:100%;height:50px;"></div>

<span>Using css translate3d and requestAnimationFrame() animate on wrapper div</span>
<div id="animWrapOuter" style="width:100%;height:50px;">
  <div id="animWrapper" style="width:100%; height:100%"></div>
</div>
2 голосов
/ 27 февраля 2020

Причина, по которой анимация мерцает, заключается в том, что она очень медленно переводится со скоростью 20 кадров в секунду (1000ms/50). Помимо низкого fps, использование setInterval также не гарантирует, что функция обратного вызова будет вызываться каждый раз (см. здесь для примера ). Чтобы сделать его более плавным, просто добавьте скорость refre sh к 60 разам в секунду, используйте requestAnimationFrame вместо setInterval для анимации объектов и увеличьте значение перевода для кадра, чтобы анимация была менее прерывистой (представьте, что вы перемещаетесь только на .2px за 3 кадра).

Вы используете 300 делений для создания анимации на временной шкале и анимации одновременно 300 делений. Это может быть немного дороже. Чтобы упростить анимацию, вы можете просто создать достаточное количество элементов div, чтобы вместить весь контейнер плюс один. Затем вам просто нужно перевести, пока самый левый div не исчезнет перед воспроизведением анимации. Это создает иллюзию непрерывной анимации, когда на самом деле это не так. Еще один более производительный способ состоит в том, чтобы анимировать только контейнер (т.е. оболочку) div.

Если вы можете изменить position или transform для создания анимации, всегда выбирайте transform. Попробуйте прочитать ссылку здесь в CSS Трюки и очень хорошее объяснение Пол Ири sh здесь.

Кроме того, вы не должны использовать setInterval оживить вещи; используйте вместо этого requestAnimationFrame. Очевидно, есть и другие методы.

Использование JS

Наконец, вот рабочий пример использования setInterval и requestAnimationFrame:

window.onload = (() => {
  var background = document.querySelector('#background')
  var interval = document.querySelector('#setInterval')
  var intervalFast = document.querySelector('#setIntervalFast')
  var raq = document.querySelector('#raq')
  var raqFast = document.querySelector('#raqFast')

  var backgroundColor, borderColor

  background.addEventListener('change', e => {
    backgroundColor = e.target.checked ? '#333333' : 'white'
    interval.style.backgroundColor = backgroundColor
    intervalFast.style.backgroundColor = backgroundColor
    raq.style.backgroundColor = backgroundColor
    raqFast.style.backgroundColor = backgroundColor
  })

  let intervalDocFrag = document.createDocumentFragment()
  let intervalFastDocFrag = document.createDocumentFragment()
  let raqDocFrag = document.createDocumentFragment()
  let raqFastDocFrag = document.createDocumentFragment()
  let current = 0
  let step = 30
  let divNeeded = Math.ceil(interval.getBoundingClientRect().width / 30) + 1 // Calculating how many divs are needed to fit one container + 1; 30 is the width of the div (29px + 1px of left border)
  for (let i = 0; i < divNeeded; i++) {
    for (let j = 0; j < 4; j++) {
      var element = document.createElement('div')
      element.style.position = 'absolute'
      element.style.height = '50px'
      element.style.width = step + 'px'

      element.style.left = current + 'px'

      element.style['border-left'] = '1px gray solid'
      if (j === 0)
        intervalDocFrag.appendChild(element)
      else if (j === 1)
        raqDocFrag.appendChild(element)
      else if (j === 2)
        intervalFastDocFrag.appendChild(element)
      else if (j === 3)
        raqFastDocFrag.appendChild(element)
    }

    current += step
  }

  interval.appendChild(intervalDocFrag)
  intervalFast.appendChild(intervalFastDocFrag)
  raq.appendChild(raqDocFrag)
  raqFast.appendChild(raqFastDocFrag)

  let intervalTranslateSlowValue = 0
  function intervalSlowAnimation() {
    if (Math.floor(intervalTranslateSlowValue) === -30) {
      intervalTranslateSlowValue = 0 // Resetting animation to create an endless timeline animating illusion
    } else {
      intervalTranslateSlowValue -= 0.064 // Gotten from 0.2 * 16 / 50
    }
    for (let child of interval.children) {
      child.style.transform = `translateX(${intervalTranslateSlowValue}px)`
    }
  }

  let intervalTranslateFastValue = 0
  function intervalFastAnimation() {
    if (Math.floor(intervalTranslateFastValue) === -30) {
      intervalTranslateFastValue = 0
    } else {
      intervalTranslateFastValue -= 0.2
    }
    for (let child of intervalFast.children) {
      child.style.transform = `translateX(${intervalTranslateFastValue}px)`
    }
  }

  function raqSlowAnimate(timeElapsed) {
    let translateValue = -1 * ((timeElapsed / (1000/60) * 0.064) % 30)
    for (let child of raq.children) {
      child.style.transform = `translateX(${translateValue}px)`
    }
    window.requestAnimationFrame(raqSlowAnimate)
  }

  function raqFastAnimate(timeElapsed) {
    let translateValue = -1 * ((timeElapsed / (1000/60) * 0.2) % 30)
    for (let child of raqFast.children) {
      child.style.transform = `translateX(${translateValue}px)`
    }
    window.requestAnimationFrame(raqFastAnimate)
  }

  window.setInterval(intervalSlowAnimation, 1000/60)
  window.setInterval(intervalFastAnimation, 1000/60)
  window.requestAnimationFrame(raqSlowAnimate)
  window.requestAnimationFrame(raqFastAnimate)
})
* {
  box-sizing: border-box;
}

#setInterval,
#setIntervalFast,
#raq,
#raqFast {
  position: relative;
  width: 100%;
  height: 50px;
  margin-bottom: 1em;
  overflow: hidden;
}
<div>
  <input type="checkbox" id="background" label="Dark">
  <label>Dark</label>
</div>

<span>Using setInterval at [1000/60]ms; translating .2px per 50ms (.064px per 16ms)</span>
<div id="setInterval"></div>

<span>Using requestAnimationFrame; translating .2px per 50ms (.064px per 16ms)</span>
<div id="raq"></div>

<span>Using setInterval at [1000/60]ms; translating .2px per 16ms</span>
<div id="setIntervalFast"></div>

<span>Using requestAnimationFrame; translating .2px per 16ms</span>
<div id="raqFast"></div>

Использование CSS Анимация (проще)

Вы также можете использовать CSS анимацию, чтобы легко создать анимацию выше:

window.onload = (() => {
  var background = document.querySelector('#background')
  var css = document.querySelector('#cssMethod')
  var cssFast = document.querySelector('#cssMethodFast')

  var backgroundColor, borderColor

  background.addEventListener('change', e => {
    backgroundColor = e.target.checked ? '#333333' : 'white'
    css.style.backgroundColor = backgroundColor
    cssFast.style.backgroundColor = backgroundColor
  })

  let cssDocFrag = document.createDocumentFragment()
  let cssFastDocFrag = document.createDocumentFragment()
  let current = 0
  let step = 30
  let divNeeded = Math.ceil(css.getBoundingClientRect().width / 30) + 1
  for (let i = 0; i < divNeeded; i++) {
    for (let j = 0; j < 2; j++) {
      var element = document.createElement('div')
      element.style.position = 'absolute'
      element.style.height = '50px'
      element.style.width = step + 'px'

      element.style.left = current + 'px'

      element.style['border-left'] = '1px gray solid'
      
      if (j == 0) css.appendChild(element)
      else if (j == 1) cssFast.appendChild(element)
    }

    current += step
  }

  css.appendChild(cssDocFrag)
  cssFast.appendChild(cssFastDocFrag)
})
* {
  box-sizing: border-box;
}

#cssMethod,
#cssMethodFast {
  position: relative;
  width: 100%;
  height: 50px;
  margin-bottom: 1em;
  overflow: hidden;
}

#cssMethod div {
  animation: 6s linear translation 0s infinite;
}

#cssMethodFast div {
  animation: 2.4s linear translation 0s infinite;
}

@keyframes translation {
  from { transform: translateX(-0px); }
  to { transform: translateX(-30px); }
}
<div>
  <input type="checkbox" id="background" label="Dark">
  <label>Dark</label>
</div>

<span>Using CSS animation; translating .2px per 50ms</span>
<div id="cssMethod"></div>

<span>Using CSS animation; translating .625px per 50ms</span>
<div id="cssMethodFast"></div>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...