• CSS & JS Effect – Statistics Counter


    效果

    当 scroll 到那些号码的时候, 号码从 0 开始跳动, 一直到最终的值.

    实现思路

    1. 一开始把号码 set to 0

    2. 使用 IntersectionObserver 监听号码出现

    3. 出现后开始累加, 一直到最终的 value. (注意, 虽然每个号码是不同的, 但是会在同一秒低到终点. 所以每个号码的累加速度是不一样的, 号码越大跑的就越快)

    搭环境

    HTML

    <body>
      <header>Lorem ipsum dolor sit.</header>
      <main>
        <p>
          <span class="number">1280</span>
          <span>px</span>
        </p>
        <p>
          <span class="number">1366</span>
          <span>px</span>
        </p>
        <p>
          <span class="number">1560</span>
          <span>px</span>
        </p>
        <p>
          <span class="number">1920</span>
          <span>px</span>
        </p>
      </main>
    </body>
    View Code

    CSS Style

    * {
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }
    
    header {
      height: 80vh;
      width: 100%;
      background-color: pink;
      display: grid;
      place-content: center;
      font-size: 4rem;
      text-align: center;
    }
    
    main {
      height: 100vh;
      display: grid;
      place-content: center;
    
      p {
        font-size: 4rem;
      }
    }
    View Code

    效果

    还没有加入 JS 所以完全没有效果.

    JavaScript Step by Step

    创建最终的 setup 函数

    export function setupStatisticsCounter(): void {}
    setupStatisticsCounter();

    definition & startup

    export function setupStatisticsCounter(): void {
      const duration = 2500;
      const interval = 50;
      const counters = Array.from(
        document.querySelectorAll<HTMLElement>(".number")
      );
    }

    1. 累加一共耗时 2.5秒, 每 50ms 跳动一次. 这里是控制体验.

    2. 把需要的 setup counter elements 找出来

    set number to zero

    for (const counter of counters) {
      counter.dataset.endNumber = counter.textContent!;
      counter.textContent = "0";
    }

    把当前的号码 set 成 0. 需要把号码保存起来哦. 不然等下就不知道要累加到多少了.

    Setup IntersectionObserver

    const io = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          const element = entry.target as HTMLElement;
          io.unobserve(element);
          startAccumulate(element, +element.dataset.endNumber!);
        }
      }
    });
    counters.forEach((counter) => io.observe(counter));
    
    function startAccumulate(element: HTMLElement, endValue: number): void {
      console.log("do accumulate", [duration, interval, element, endValue]);
    }

    当 counter intersecting 的时候开始执行累加. 累加函数只有接口还没有具体实现. 

    每一个 counter 都需要 observe 哦. 而且一旦开始累加就可以 unobserve 了.

    累加函数

    function startAccumulate(element: HTMLElement, endValue: number): void {
      const increment = endValue / (duration / interval);
      const intervalNumber = setInterval(() => {
        let currentNumber = +element.textContent!;
        if (currentNumber < endValue) {
          element.textContent = Math.ceil(
            (currentNumber += increment)
          ).toString();
        } else {
          element.textContent = endValue.toString();
          clearInterval(intervalNumber);
        }
      }, interval);
    }

    一个 interval 不断累加, 直到达到最终值. 唯一要注意的是它的 increment.

    通过 endValue / (duration / interval) 就可以计算出不同 counter 的 increment, 这样就可以确保不同号码的 counter 都会在同一时间结束. 

    因为每一个 counter 的 increment 是不相同的, 越大的 endValue increment 也越大.

    Final code

    export function setupStatisticsCounter(): void {
      const duration = 2500;
      const interval = 50;
      const counters = Array.from(
        document.querySelectorAll<HTMLElement>(".number")
      );
    
      for (const counter of counters) {
        counter.dataset.endNumber = counter.textContent!;
        counter.textContent = "0";
      }
    
      const io = new IntersectionObserver((entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            const element = entry.target as HTMLElement;
            io.unobserve(element);
            startAccumulate(element, +element.dataset.endNumber!);
          }
        }
      });
      counters.forEach((counter) => io.observe(counter));
    
      function startAccumulate(element: HTMLElement, endValue: number): void {
        const increment = endValue / (duration / interval);
        const intervalNumber = setInterval(() => {
          let currentNumber = +element.textContent!;
          if (currentNumber < endValue) {
            element.textContent = Math.ceil(
              (currentNumber += increment)
            ).toString();
          } else {
            element.textContent = endValue.toString();
            clearInterval(intervalNumber);
          }
        }, interval);
      }
    }
    setupStatisticsCounter();
    View Code

    效果

    字体宽度的问题

    仔细看会发现, 累加的时候字体的宽度是一直在变化的. 从 0 到 1920 宽度自然增加了.

    这种跳动的体验有时候不太好.

    解决思路

    1. 一开始的时候先获取最终值时的 width, before set to zero

    2. 然后把这个 width apply 到 span 上去. 这样 set to zero 后, width 依然是最终的 width.

    3. 在累加完后移除 width

    难点

    由于字体加载需要时间, 所以不可以一开始就获取 width, 需要等待字体加载完后才是最终的 width. 可以使用 CSS Font Loading API

    即便如此, 如果不是使用 等宽字体, 最终的 width 依然不一定满足累加时的 width 最大值. 所以还是可能会出现号码超出 width 的情况. 

    所以呢, 最完美的情况是, 使用等宽字体. 要不然不管怎么搞最终都不完美.

    方案一, set 最终值的 width, 缺点累加时可能超出这个 width.

    方案二, 用 ch unit 配上 length 做 width, 缺点最终值可能小于这个 width

    下面是加了方案一的 JS 代码

    export function setupStatisticsCounter(): void {
      // note 隐患:
      // 如果是不等宽字体, 在累加的时候号码可能会超出 width 哦, right way 是用等宽字体, 比如 Roboto
    
      // note 解忧:
      // 需要等 fonts 加载好才能 set, 不然会跳一下.
      document.fonts.ready.then(() => {
        const duration = 2500;
        const interval = 50;
        const counters = Array.from(document.querySelectorAll<HTMLElement>('.number'));
    
        for (const counter of counters) {
          counter.dataset.endNumber = counter.textContent!;
          // note 解忧:
          // 给 inline-block 是因为要 set width
          if (window.getComputedStyle(counter).display === 'inline') {
            counter.style.display = 'inline-block';
          }
          // 给 width 是为了不要累加的时候会跳
          counter.style.width = `${counter.offsetWidth}px`;
          counter.textContent = '0';
        }
    
        const io = new IntersectionObserver(entries => {
          for (const entry of entries) {
            if (entry.isIntersecting) {
              const element = entry.target as HTMLElement;
              io.unobserve(element);
              startAccumulate(element, +element.dataset.endNumber!);
            }
          }
        });
        counters.forEach(counter => io.observe(counter));
    
        function startAccumulate(element: HTMLElement, endValue: number): void {
          const increment = endValue / (duration / interval);
          const intervalNumber = setInterval(() => {
            let currentNumber = +element.textContent!;
            if (currentNumber < endValue) {
              element.textContent = Math.ceil((currentNumber += increment)).toString();
            } else {
              element.textContent = endValue.toString();
              clearInterval(intervalNumber);
              element.style.removeProperty('width');
              element.style.removeProperty('display');
            }
          }, interval);
        }
      });
    }
    View Code
  • 相关阅读:
    A magic method allowing a third variable used in comparison function of std::sort
    Create a wireframe box in rviz but not using any other extra tools (unfinished)
    Three methods to iterate every point in pointcloud of PCL(三种遍历点云的方式)
    Environment Perception: 3D Truss Environment Mapping and Parametric Expression Extraction
    shell脚本练习02--求字符串的长度
    shell脚本练习01
    shell脚本,循环的记录
    linux 备份最近一天的文件
    mybatis和java一些知识记录
    第8章
  • 原文地址:https://www.cnblogs.com/keatkeat/p/16366408.html
Copyright © 2020-2023  润新知