# 动画案例

整理一些日常用到的一些动画,以便想用的时候随时可以取用。当然更多的可以参考animate.css

# 1、数字变化的动画

如下所示

0
1000
$1234567.89

/*
     * 数字变化动画函数
     * @param {Object} options - 配置选项
     * @param {number} options.from - 起始值
     * @param {number} options.to - 目标值
     * @param {number} [options.duration=1000] - 动画时长(毫秒)
     * @param {function} options.onUpdate - 每帧更新回调 (formattedValue, rawValue) => {}
     * @param {function} [options.formatter] - 数字格式化函数
     * @param {string} [options.easing='easeInOutQuad'] - 缓动函数
     * @returns {function} cancel - 取消动画函数
     */
    function animateNumber({
      from,
      to,
      duration = 1000,
      onUpdate,
      formatter = (n) => n.toLocaleString('zh-CN', {
        maximumFractionDigits: 2
      }),
      easing = 'easeInOutQuad'
    }) {
      const startValue = Number(from);
      const endValue = Number(to);
      const startTime = performance.now();
      let rafId = null;

      // 缓动函数库
      const easingFunctions = {
        linear: t => t,
        easeIn: t => t * t,
        easeOut: t => t * (2 - t),
        easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
        easeOutCubic: t => 1 + Math.pow(t - 1, 3),
      };

      const ease = easingFunctions[easing] || easingFunctions.easeInOutQuad;

      function update(currentTime) {
        const elapsed = currentTime - startTime;
        const progress = Math.min(elapsed / duration, 1);
        const easedProgress = ease(progress);

        // 计算当前值(保留精度)
        const current = startValue + (endValue - startValue) * easedProgress;

        // 格式化并回调
        if (onUpdate) {
          onUpdate(formatter(current), current);
        }

        if (progress < 1) {
          rafId = requestAnimationFrame(update);
        } else {
          // 确保最终值准确
          if (onUpdate) {
            onUpdate(formatter(endValue), endValue);
          }
        }
      }

      rafId = requestAnimationFrame(update);

      // 返回取消函数
      return () => {
        if (rafId) {
          cancelAnimationFrame(rafId);
          rafId = null;
        }
      };
    }

    // 使用示例
    const cancel = animateNumber({
      from: 0,
      to: 1000,
      duration: 2000,
      onUpdate: (formatted, raw) => {
        document.getElementById('num').textContent = formatted;
        console.log(raw); // 原始值,可用于其他逻辑
      },
      // formatter: (n) => n.toLocaleString('zh-CN', {
      //   minimumFractionDigits: 2,
      //   maximumFractionDigits: 2
      // }),
      formatter: n => n.toFixed(0),
    });

    // 2秒后自动取消(如果动画未结束)
    // setTimeout(cancel, 2000);

    // 负数变化示例
    animateNumber({
      from: 1000,
      to: -1000,
      duration: 1500,
      onUpdate: (formatted) => {
        document.getElementById('counter-negative').textContent = formatted;
      },
      formatter: (n) => {
        const abs = Math.abs(n);
        return (n < 0 ? '-' : '') + abs.toLocaleString('zh-CN');
      }
    });

    // 大数字示例(带货币格式)
    animateNumber({
      from: 1234567.89,
      to: 9876543.21,
      duration: 3000,
      easing: 'easeOutCubic',
      onUpdate: (formatted) => {
        document.getElementById('money').textContent = '$' + formatted;
      },
      formatter: (n) => n.toLocaleString('en-US', {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2
      })
    });

下面是自己瞎写的, 可能不是很好用

function changeNum(config, cb = () => {}) {
  let { from, to, duration = 1000 } = config;
  if (from === to) {
    cb({
        data: to,
        done: true
      });
      return;
  }
  const step = Math.floor((to - from) / 20);
  const direction = to - from >= 0; // true 表示正向
  let start = undefined;
  cb({
    data: from,
    done: false
  });
  function callback(timestamp) {
    if (start === undefined) {
      start = timestamp;
    }
    let elapsed = timestamp - start;
    if (elapsed >= duration) {
      cb({
        data: to,
        done: true
      });
      return;
    }
    let num;
    if (direction) {
      num = Math.max(Math.floor(Math.random() * step), 1);
    } else {
      num = Math.min(Math.floor(Math.random() * step), -1);
    }
    from = from + num;
    if ((direction && from >= to) || (!direction && from <= to)) {
      from = to;
    }
    if (from === to) {
      console.log(elapsed);
      cb({
        data: from,
        done: true
      });
      return;
    }
    cb({
      data: from,
      done: false
    });
    window.requestAnimationFrame(callback);
  }
  window.requestAnimationFrame(callback);
}

用法

changeNum(
  {
    from: 0,
    to: 1000
  },
  param => {
    el.textContent = param.data.toString();
  }
);

# 2、shake 摇动

主要用于点击某些按钮需要摇晃一下。

.shake {
  animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  transform: translate3d(0, 0, 0);
}
@keyframes shake {
  10%,
  90% {
    transform: translate3d(-1px, 0, 0);
  }
  20%,
  80% {
    transform: translate3d(2px, 0, 0);
  }
  30%,
  50%,
  70% {
    transform: translate3d(-4px, 0, 0);
  }
  40%,
  60% {
    transform: translate3d(4px, 0, 0);
  }
}

# 3、点击效果

尤其是移动端,可以稍微用一下。不过大部分时候还是用button的伪类来做好一点

.clickable:active {
    transform: translateY(2px);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    background-color: #f8f9fa;
}
上次更新: 1/29/2026, 9:17:42 AM