# 轮播

说下简单的思路吧, 就是一般来说首尾是一样的元素, 当最后一个一样的元素动画完成之后, 把元素的定位设置成初始定位, 然后重新开始动画, 轮播的定时可以使用setInterval 各种宽高之类的动态计算都可以封装起来。动画过度是匀速还是变速,这个步进值也可以自己设置。

# 1、简单的无缝滚动

不用js只是用css来实现,注意第一个和最后一个节点是一样的即可。translateX滚动的距离应该是length-1乘以子项的宽度。

示例如下,

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <style>
      * {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
      }
      @keyframes Scroll {
        from {
          transform: translateX(0);
        }
        to {
          transform: translateX(-1200px);
        }
      }
      .container {
        width: 300px;
        height: 200px;
        box-sizing: content-box;
        border: 1px solid #ccc;
        overflow: hidden;
        margin: 100px auto;
      }
      .container > ul {
        width: 1500px;
        overflow: hidden;
        list-style: none;
        animation-name: Scroll;
        animation-duration: 4s;
        animation-timing-function: linear;
        animation-iteration-count: infinite;
      }
      .container:hover > ul {
        animation-play-state: paused;
      }
      .container > ul > li {
        /* font-size: 0; */
        width: 300px;
        height: 200px;
        float: left;
      }
      .container > ul > li:nth-child(2n) {
        background-color: #aaa;
      }
      .container > ul > li:nth-child(2n-1) {
        background-color: #ccc;
      }
    </style>
    <div class="container" id="scroll">
      <!-- 1800 - 600 = 1200 -->
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>1</li>
      </ul>
    </div>
  </body>
</html>

如果想动态计算某些值的话, 提供一个粗暴简单的思路,直接用css覆盖。当然也可以使用window.requestAnimationFrame来实现。

<script>
  const style = document.createElement('style');
  style.setAttribute('data-v', 'custom')
  const duration = 10;
  style.textContent = `
  .container > ul > li:nth-child(2n) {
    background-color: red;
  }
  .container > ul {
    animation-duration: ${duration}s;
  }
  `;
  document.body.appendChild(style);
</script>

# 2、轮播

轮播也是一样的原理, 无非就是改变内容的移动的值,也可以使用定位来实现, 按照设定的参数修改left的值即可了。

接下来实现一个原理性的轮播示例, 就使用js来达到一定的动态效果。 我自己瞎写的一个demo。明白原理即可

动画用的 transition 来实现的,也可以使用定时器或者requestAnimatiton来实现

# 2.1 transition

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>轮播</title>
  </head>
  <body>
    <style>
      * {
        padding: 0;
        margin: 0;
        list-style: none;
        box-sizing: border-box;
      }
      .carouse-wrapper {
        width: 600px;
        height: 400px;
        margin: 50px auto;
        box-sizing: content-box;
        border: 1px solid #ccc;
        /* position: relative; */
        background-color: #fff;
        overflow: hidden;
      }
      .carousel-content {
        height: 100%;
        overflow: hidden;
      }
      .carousel-content__item {
        font-size: 60px;
        line-height: 400px;
        text-align: center;
      }
      .carousel-content__item:nth-child(2n-1) {
        background-color: #aaa;
      }
    </style>
    <div class="carouse-wrapper" id="carouse_wrapper">
      <!-- <ul class="carousel-content">
        <li class="carousel-content__item"></li>
      </ul> -->
    </div>
    <script>
      function initCarousel(el, size, itemList = [], itemFactory = () => {}) {
        if (typeof el === 'string') {
          el = document.querySelector(`#${el}`);
        }
        if (!el.style.position || window.getComputedStyle(el).position === 'static') {
          el.style.position = 'relative';
        }

        const { width, height } = size;
        const carouselContentEl = document.createElement('ul');
        carouselContentEl.classList.add('carousel-content');
        carouselContentEl.style.width = width * (itemList.length + 1) + 'px';
        carouselContentEl.style.left = '0px';
        carouselContentEl.style.top = '0px';
        carouselContentEl.style.position = 'absolute';
        carouselContentEl.style.transition = 'left 1s linear';

        const itemsFragment = document.createDocumentFragment();
        const _itemList = [...itemList, itemList[0]];
        _itemList.forEach((item, index) => {
          const liEl = document.createElement('li');
          liEl.classList.add('carousel-content__item');
          liEl.setAttribute('data-index', index + '');
          liEl.style.width = width + 'px';
          liEl.style.height = height + 'px';
          liEl.style.float = 'left';

          if (itemFactory && typeof itemFactory === 'function') {
            liEl.appendChild(itemFactory(item, index));
          }

          itemsFragment.appendChild(liEl);
        });

        carouselContentEl.append(itemsFragment);

        el.appendChild(carouselContentEl);

        if (itemList.length <= 1) {
          return;
        }
        // return;
        let _index = 0;
        const __carouse_timer = setInterval(() => {
          if (_index == itemList.length + 1) {
            _index = 0;
            carouselContentEl.style.transition = 'none';
            carouselContentEl.style.left = '0px';
            _index++;
            setTimeout(() => {
              carouselContentEl.style.transition = 'left 1s linear';
              carouselContentEl.style.left = 0 - _index * width + 'px';
            }, 0);
            return;
          }

          carouselContentEl.style.left = 0 - _index * width + 'px';
          _index++;
        }, 2000);
        return () => {
          clearInterval(__carouse_timer);
        };
      }

      const width = 600,
        height = 400;
      initCarousel(
        'carouse_wrapper',
        {
          width,
          height
        },
        [1, 2, 3],
        data => {
          const span = document.createElement('span');
          span.textContent = data;
          return span;
        }
      );
    </script>
  </body>
</html>

# 2.2 使用 requestAnimationFrame 来实现动画

简单写了一个动画函数, 60HZ的屏幕 step 函数会在一秒内被调用60次, 144hz的就会144次/分钟, 所以每一帧的动画距离, 也就是left的变化距离, 要简单根据帧的频率计算一下。比如需要移动 600px,要在2000ms内完成,屏幕是60hz,那么一帧大约就是16.7ms。

let interval = ((0 - 600) / 2000) * 16.7; 约等于-5 也就是每一帧需要移动5px。动画大约会在2s左右完成。

如果是144hz, let interval = ((0 - 600) / 2000) * 6.9; 约等于-2.08 也就是每一帧移动2.08px,动画会更加的细腻。

function doAnimate(el, from, to, duration) {
  if (from === to) {
    el.style.left = to + 'px';
    return;
  }
  // let interval = to - from > 0 ? 10 : -10;
  let interval = ((to - from) / duration) * keyFramesInterval;
  console.debug('interval = ', interval);
  let start, previousTimeStamp;
  let done = false;
  let _left = from;
  el.style.left = _left + 'px';
  console.time('完成动画');
  function step(timestamp) {
    if (start === undefined) {
      start = timestamp;
    }
    //   console.log(_left);
    _left += interval;
    if (interval > 0) {
      _left = Math.min(_left, to);
    } else {
      _left = Math.max(_left, to);
    }
    el.style.left = _left + 'px';
    if (_left !== to) {
      window.requestAnimationFrame(step);
    } else {
      // console.log(_left);
      console.timeEnd('完成动画');
    }
  }
  window.requestAnimationFrame(step);
}

完整示例如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>轮播</title>
  </head>
  <body>
    <style>
      * {
        padding: 0;
        margin: 0;
        list-style: none;
        box-sizing: border-box;
      }
      .carouse-wrapper {
        width: 600px;
        height: 400px;
        margin: 50px auto;
        box-sizing: content-box;
        border: 1px solid #ccc;
        /* position: relative; */
        background-color: #fff;
        overflow: hidden;
      }
      .carousel-content {
        height: 100%;
        overflow: hidden;
      }
      .carousel-content__item {
        font-size: 60px;
        line-height: 400px;
        text-align: center;
      }
      .carousel-content__item:nth-child(2n-1) {
        background-color: #aaa;
      }
    </style>
    <button onclick="cancel()">cancel</button>
    <div class="carouse-wrapper" id="carouse_wrapper">
      <!-- <ul class="carousel-content">
        <li class="carousel-content__item"></li>
      </ul> -->
    </div>
    <script>
      function getKeyFramesInterval() {
        return new Promise(resolve => {
          let start;
          function foo(timestamp) {
            if (start === undefined) {
              start = timestamp;
              window.requestAnimationFrame(foo);
              return;
            }
            if (start && timestamp) {
              resolve(timestamp - start);
            }
          }
          window.requestAnimationFrame(foo);
        });
      }
      getKeyFramesInterval().then(res => {
        console.log('getKeyFramesInterval', res);
      });

      let keyFramesInterval = 16.66666;

      /* 
        假设要在2000ms内完成动画
        */
      function doAnimate(el, from, to, duration) {
        if (from === to) {
          el.style.left = to + 'px';
          return;
        }
        // let interval = to - from > 0 ? 10 : -10;
        let interval = ((to - from) / duration) * keyFramesInterval;
        console.debug('interval = ', interval);
        let start, previousTimeStamp;
        let done = false;
        let _left = from;
        el.style.left = _left + 'px';
        console.time('完成动画');
        function step(timestamp) {
          if (start === undefined) {
            start = timestamp;
          }
          //   console.log(_left);
          _left += interval;
          if (interval > 0) {
            _left = Math.min(_left, to);
          } else {
            _left = Math.max(_left, to);
          }
          el.style.left = _left + 'px';
          if (_left !== to) {
            window.requestAnimationFrame(step);
          } else {
            // console.log(_left);
            console.timeEnd('完成动画');
          }
        }
        window.requestAnimationFrame(step);
      }

      function initCarousel(el, size, itemList = [], itemFactory = () => {}) {
        if (typeof el === 'string') {
          el = document.querySelector(`#${el}`);
        }
        if (!el.style.position || window.getComputedStyle(el).position === 'static') {
          el.style.position = 'relative';
        }

        const { width, height } = size;
        const carouselContentEl = document.createElement('ul');
        carouselContentEl.classList.add('carousel-content');
        carouselContentEl.style.width = width * (itemList.length + 1) + 'px';
        carouselContentEl.style.left = '0px';
        carouselContentEl.style.top = '0px';
        carouselContentEl.style.position = 'absolute';
        // carouselContentEl.style.transition = 'left 1s linear';

        const itemsFragment = document.createDocumentFragment();
        const _itemList = [...itemList, itemList[0]];
        _itemList.forEach((item, index) => {
          const liEl = document.createElement('li');
          liEl.classList.add('carousel-content__item');
          liEl.setAttribute('data-index', index + '');
          liEl.style.width = width + 'px';
          liEl.style.height = height + 'px';
          liEl.style.float = 'left';

          if (itemFactory && typeof itemFactory === 'function') {
            liEl.appendChild(itemFactory(item, index));
          }

          itemsFragment.appendChild(liEl);
        });

        carouselContentEl.append(itemsFragment);

        el.appendChild(carouselContentEl);

        if (itemList.length <= 1) {
          return;
        }
        // return;
        let from = 0,
          step = -600;
        let to = from + step;
        const __carouse_timer = setInterval(() => {
          if (from === 0) {
            carouselContentEl.style.left = '0px';
          }
          doAnimate(carouselContentEl, from, to, 2000);
          if (to === itemList.length * step) {
            from = 0;
            to = from + step;
            return;
          }
          from = to;
          to = from + step;
        }, 3000);
        return () => {
          clearInterval(__carouse_timer);
        };
      }

      let _cancel;
      getKeyFramesInterval().then(a => {
        keyFramesInterval = a;
        const width = 600,
          height = 400;
        _cancel = initCarousel(
          'carouse_wrapper',
          {
            width,
            height
          },
          [1, 2, 3, 4],
          data => {
            const span = document.createElement('span');
            span.textContent = data;
            return span;
          }
        );
      });

      function cancel() {
        _cancel();
      }
    </script>
  </body>
</html>

上面这些只是基础和原理,我也只是提供一个思路。更多效果和动画函数,可以使用https://swiperjs.com/ (opens new window)

上次更新: 4/1/2025, 9:33:10 AM