Javascript list cards fadeOut effect

先看效果

  • Card 1
  • Card 2
  • Card 3
  • Card 4
  • Card 5
  • Card 6
  • Card 7
  • Card 8
  • Card 9
  • Card 10

那么怎么实现呢,首先是 HTML 结构:

<ul>
  <li>Card 1</li>
  <li>Card 2</li>
  <li>Card 3</li>
  <li>Card 4</li>
  <li>Card 5</li>
  <li>Card 6</li>
  <li>Card 7</li>
  <li>Card 8</li>
  <li>Card 9</li>
  <li>Card 10</li>
</ul>

只要是个列表就行,随便什么div都可以。

样式也随意,只需要注意 ulposition 不是能初始值,可以给一个 relative,否则会影响计算子元素的 offsetTop

定义动画函数,并监听 ul 的滚动事件

function fadeout(el) {
  const len = el.childElementCount;
  let fadeOutAnimating = false;
  function fadeoutEffect() {
    fadeOutAnimating = true;
    // animation logic
    fadeOutAnimating = false;
  }
  function animate() {
    if (!fadeOutAnimating) {
      requestAnimationFrame(fadeoutEffect);
    }
  }
  el.addEventListener('scroll', animate);
}

动画逻辑就是当滚动进行时,当子元素超出父元素可视范围时,将滚动距离与子元素的高度做转换,使用 requestAnimationFrame 设置子元素大小及透明度样式,那么随着滚动距离越大,子元素就越小,不透明度越来越低

function fadeoutEffect() {
  fadeOutAnimating = true;
  for (let i = 0; i < len; i++) {
    if (el.scrollTop > el.children[i].offsetTop) {
      const scrolling = el.scrollTop - el.children[i].offsetTop;
      const scaling = Math.min(1, Math.max(0, (el.children[i].offsetHeight - scrolling * 0.2) / el.children[i].offsetHeight));
      if (!el.children[i].classList.contains('fadeOutScalingTop')) {
        el.children[i].classList.add('fadeOutScalingTop');
      }
      const opacity = scaling * 100 < 96 ? scaling ** 8 : 1;
      if (opacity > 0) {
        el.children[i].style.opacity = opacity;
        el.children[i].style.transform = `translate3d(0, ${scrolling}px, 0) scale(${scaling})`;
      }
    } else if (el.children[i].classList.contains('fadeOutScalingTop')) {
      el.children[i].classList.remove('fadeOutScalingTop');
      el.children[i].style.transform = 'none';
      el.children[i].style.opacity = 1;
    }
  }
  fadeOutAnimating = false;
}

动画效果中给子元素添加了 className fadeOutScalingTop, 主要是给子元素设置了 transform-origin 属性:

.fadeOutScalingTop {
  position: relative;
  position: -webkit-sticky;
  position: sticky;
  -webkit-transform-origin: center top;
  transform-origin: center top;
}

为了性能好一些可以给子元素设置上 will-change: transform 属性。

现在动效逻辑就完成了,调用即可:

fadeout(document.querySelector('ul')[0]);

完整代码

/**
 * Copyright ThomasChan.
 * This source code is licensed under the MIT license.
 */

<style>
  #demo {
    position: relative;
    width: 200px;
    height: 400px;
    overflow: overlay;
    border: 1px solid #dadada;
    border-radius: 2px;
    box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
    padding: 0px 15px;
  }
  #demo li {
    display: flex;
    justify-content: center;
    align-items: center;
    list-style: none;
    margin: 15px auto;
    height: 100px;
    border: 1px solid #dadada;
    border-radius: 2px;
    box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
    will-change: transform;
    background-color: white;
  }
  .fadeOutScalingTop {
    position: relative;
    position: -webkit-sticky;
    position: sticky;
    -webkit-transform-origin: center top;
    transform-origin: center top;
  }
</style>
<ul id="demo">
  <li>Card 1</li>
  <li>Card 2</li>
  <li>Card 3</li>
  <li>Card 4</li>
  <li>Card 5</li>
  <li>Card 6</li>
  <li>Card 7</li>
  <li>Card 8</li>
  <li>Card 9</li>
  <li>Card 10</li>
</ul>
<script>
  function fadeout(el) {
    const len = el.childElementCount;
    let fadeOutAnimating = false;
    const childSize = 100;
    function fadeoutEffect() {
      fadeOutAnimating = true;
      for (let i = 0; i < len; i++) {
        if (el.scrollTop > el.children[i].offsetTop) {
          const scrolling = el.scrollTop - el.children[i].offsetTop;
          const scaling = Math.min(1, Math.max(0, (childSize - scrolling * 0.2) / childSize));
          const opacity = scaling * 100 < 96 ? scaling ** 8 : 1;
          if (opacity > 0) {
            if (!el.children[i].classList.contains('fadeOutScalingTop')) {
              el.children[i].classList.add('fadeOutScalingTop');
            }
            el.children[i].style.opacity = opacity;
            el.children[i].style.transform = `translate3d(0, ${scrolling}px, 0) scale(${scaling})`;
          }
        } else if (el.children[i].classList.contains('fadeOutScalingTop')) {
          el.children[i].classList.remove('fadeOutScalingTop');
          el.children[i].style.transform = 'none';
          el.children[i].style.opacity = 1;
        }
      }
      fadeOutAnimating = false;
    }
    function animate() {
      if (!fadeOutAnimating) {
        requestAnimationFrame(fadeoutEffect);
      }
    }
    el.addEventListener('scroll', animate);
  }
  window.onload = function() {
    fadeout(document.querySelector('#demo'));
  }
</script>