那个让我熬夜到两点的数字滚动效果,原来是这样实现的!

8,774 阅读3分钟

那个让我熬夜到两点的数字滚动效果,原来是这样实现的!

缘起:被某度首页种草的计数器

昨晚刷着手机,突然看到某度首页那个酷炫的AI调用次数计数器——数字像滚筒洗衣机一样唰唰滚动,最后稳稳停在目标数值。作为一个前端小菜鸟,我眼睛"叮"地亮了起来:"这个效果!我一定要自己实现出来!"

踩坑之路:从"简单"到"真香"

第一版尝试:CSS关键帧硬刚

/* 天真的初始方案 */
@keyframes roll {
  0% { transform: translateY(0); }
  100% { transform: translateY(-600px); }
}

结果数字像跳楼一样直上直下,完全没有丝滑的滚动感。这时候我才意识到,某度那个效果的精髓在于模拟真实物理运动的惯性

凌晨一点的顿悟时刻

当我打开开发者工具仔细分析某度实现时,发现了三个关键点:

  1. 每个数字位的滚动速度不同,像水流一样依次启动
  2. 数字到达目标后还会轻微回弹,就像刹车时的惯性
  3. 滚动轨迹不是匀速,而是先快后慢的缓动效果

核心实现:物理惯性模拟的奥秘

灵魂参数表(调了整整一下午!)

const CONFIG = {
  DURATION: 2000,       // 总动画时长(手抖调成200会鬼畜)
  ROLL_COUNT: 2,        // 额外空转圈数(像洗衣机甩干)
  DELAY_BETWEEN_DIGITS: 40, // 数字间延迟(制造波浪感)
  DIGIT_HEIGHT: 60,     // 单个数字高度(改这个要老命)
};

让我掉了一把头发的核心算法

// 这个公式我对着墙壁比划了半小时才理解
const targetY = -(CONFIG.ROLL_COUNT * 10 + targetDigit) * CONFIG.DIGIT_HEIGHT;

// 举个例子:要显示数字7,实际要滚动(2圈*10 +7)=27个数字的高度
// 这样就有"先转两圈再慢慢停到7"的效果!

最终折腾出来的DOM结构

<!-- 每个数字位都是独立滚筒 -->
<div class="digit-container">
  <div class="digit-list" style="transition: transform 1.8s ease-out">
    <div class="digit">0</div>
    <div class="digit">1</div>
    ...(此处省略20个数字)
    <div class="digit">9</div>
  </div>
</div>

🌟完整源码(含详细注释版)

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>数字滚动计数器效果</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f5f5f5;
      }

      .counter-container {
        display: flex;
        background-color: #fff;
        border-radius: 8px;
        padding: 20px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      }

      .digit-container {
        width: 40px;
        height: 60px;
        overflow: hidden;
        position: relative;
        margin: 0 2px;
      }

      .digit-list {
        position: absolute;
        transition: transform 2s ease-in-out;
        transform: translateY(0);
      }

      .digit {
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 36px;
        height: 60px;
        font-weight: bold;
      }

      .prefix,
      .suffix {
        font-size: 24px;
        margin: 0 10px;
        align-self: center;
      }

      #counter {
        display: flex;
      }
    </style>
  </head>
  <body>
    <div class="counter-container">
      <div class="prefix">今日已解决</div>
      <div id="counter"></div>
      <div class="suffix">个问题</div>
    </div>

    <script>
      // 配置参数
      const CONFIG = {
        DURATION: 2000, // 动画持续时间(毫秒)
        ROLL_COUNT: 2, // 数字滚动的额外循环次数
        DELAY_BETWEEN_DIGITS: 40, // 数字之间的延迟时间(毫秒)
        DIGIT_HEIGHT: 60, // 数字高度(像素)
        TARGET_NUMBER: 7140909, // 目标数字
      };

      function createCounter(targetNumber) {
        const counterEl = document.getElementById("counter");
        const digits = targetNumber.toString().split("");

        // 为每个数字创建容器和滚动效果
        digits.forEach((digit, index) => {
          const digitContainer = document.createElement("div");
          digitContainer.className = "digit-container";

          const digitList = document.createElement("div");
          digitList.className = "digit-list";

          // 设置动画延迟,从左到右依次延迟
          const delay =
            (digits.length - index - 1) * CONFIG.DELAY_BETWEEN_DIGITS;
          digitList.style.transition = `transform ${
            CONFIG.DURATION - delay
          }ms ease-in-out`;

          // 创建滚动序列(0-9 重复多次)
          for (let i = 0; i <= CONFIG.ROLL_COUNT; i++) {
            for (let j = 0; j < 10; j++) {
              const digitEl = document.createElement("div");
              digitEl.className = "digit";
              digitEl.textContent = j;
              digitList.appendChild(digitEl);
            }
          }

          digitContainer.appendChild(digitList);
          counterEl.appendChild(digitContainer);
        });

        // 开始动画
        setTimeout(() => {
          animateToTarget(targetNumber);
        }, 100);
      }
      // 启动动画
      function animateToTarget(targetNumber) {
        const digits = targetNumber.toString().split("");
        const digitLists = document.querySelectorAll(".digit-list");
      
        digitLists.forEach((list, i) => {
          const targetDigit = parseInt(digits[i], 10);
          //关键计算公式!(总滚动距离 = 惯性圈数*10 + 目标数字)
          const extraRolls = CONFIG.ROLL_COUNT * 10;
          const targetY = -(extraRolls + targetDigit) * CONFIG.DIGIT_HEIGHT;

          list.style.transform = `translateY(${targetY}px)`;
        });
      }

      // 初始化计数器
      window.onload = function () {
        createCounter(CONFIG.TARGET_NUMBER);
      };
    </script>
  </body>
</html>

效果:🎉🎉

2025-05-2709.58.28-ezgif.com-video-to-gif-converter.gif

后记:向某度工程师致敬

当我终于复现出这个效果时,已经是凌晨两点半。看着屏幕上流畅滚动的数字,突然想起《月亮与六便士》里的一句话:"美是难的"。这个看似简单的效果,背后是对用户体验的极致追求——多一圈则浮夸,少一圈则生硬的微妙平衡。

或许这就是前端的魅力所在:用逻辑雕琢美感,让理性的数字跳起感性的芭蕾。如果这个实现过程对你有启发,欢迎点赞+关注+转发,也欢迎在评论区留下你的优化方案~