数字递增递减动画

数字直接变化

  • 借助 VueNumberScroll 组件
<template>
  <div>
    <VueNumberScroll
      class="num"
      :start="startNum"
      :end="endNum"
      :times="10"
      :speed="100"
      :format="num => padZeroLeft(num, numLength)"
    ></VueNumberScroll>
  </div>
</template>

<script>
import VueNumberScroll from 'vue-number-scroll';

export default {
  components: {
    VueNumberScroll
  },
  props: {
    number: {
      type: Number,
      default: 0,
    },
    numLength: {
      type: Number,
      default: 6,
    },
    defaultNumber: {
      type: Number,
      default: 0,
    },
  },
  watch: {
    number: {
      handler(newValue, oldValue) {
        this.startNum = oldValue || this.defaultNumber;
        this.endNum = newValue;
      },
      immediate: true,
    }
  },
  methods: {
    padZeroLeft(num = 0, len = 0) {
      return (`${num}`).padStart(len, '0');
    },
  },
  data() {
    return {
      startNum: 0,
      endNum: 0,
    };
  },
};
</script>

<style lang="scss" scoped>
.num {
  font-size: 1rem;
  display: flex;
  justify-content: center;
}
</style>

数字像记分牌一样翻转

<template>
  <div>
    <!-- start-1 数字翻转盒子 -->
    <div class="digital-flip-box">
      <!-- start-2 数字翻转盒子 -->
      <div
        v-for="(curDigital, index) in nowNumArr"
        :key="index"
        class="digital-single-box"
      >
        <!-- start-3 数字翻转盒子 -->
        <div
          v-for="curNumber in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
          :key="curNumber"
          :class="{
            'pre': !increaseAni ? curNumber - 1 === +curDigital : false,
            'active-pre': !increaseAni ? curNumber === +curDigital : false,
            'active-next': increaseAni ? curNumber === +curDigital : false,
            'next': increaseAni ? curNumber + 1 === +curDigital : false,
          }"
          class="digital-single-list"
        >
          <!-- 上部分数字和阴影 -->
          <div class="digit-top">
            <span class="digit-num">{{curNumber}}</span>
          </div>
          <div class="shadow-top"></div>
          <!-- 下部分数字和阴影 -->
          <div class="digit-bottom">
            <span class="digit-num">{{curNumber}}</span>
          </div>
          <div class="shadow-bottom"></div>
        </div>
        <!-- end-3 -->
      </div>
      <!-- end-2 -->
    </div>
    <!-- end-1 -->
  </div>
</template>

<script>
export default {
  props: {
    // 最新数字
    number: {
      type: Number,
      default: 0,
    },
    // 数字长度
    numLength: {
      type: Number,
      default: 6,
    },
    // 默认初始数值
    defaultNumber: {
      type: Number,
      default: 0,
    },
    duration: {
      type: Number,
      default: 3000,
    },
  },
  watch: {
    number: {
      handler(newValue, oldValue) {
        this.startNum = oldValue;
        this.endNum = newValue;
        // 处理数字动画
        this.handleNumAni();
      },
      immediate: true,
    }
  },
  data() {
    return {
      startNum: 0,
      endNum: 0,
      nowNumArr: [],
      increaseAni: true, // 是否递增动画,true=递增,false=递减
      decreaseTimer: null, // 递减定时器
      increaseTimer: null, // 递增定时器
    };
  },
  methods: {
    // 处理数字
    handleNumAni() {
      // 结束值等于开始值
      if (this.endNum === this.startNum) return;
      // 没有结束值
      const noEndNum = (!this.endNum && this.endNum !== 0) || this.endNum < 0;
      if (noEndNum) {
        this.nowNumArr = this.transformNumArr(this..defaultNumber, this.numLength);
        return;
      }
      // 没有开始值
      const noStartNum = (!this.startNum && this.startNum !== 0) || this.startNum < 0;
      if (noStartNum) {
        this.nowNumArr = this.transformNumArr(this..endNum, this.numLength);
        return;
      }
      // 执行递增、递减动画
      const doMethods = { increase: this.doIncreaseAni, decrease: this.doDecreaseAni };
      this.nowNumArr = this.transformNumArr(this.startNum, this.numLength);
      this.increaseAni = this.startNum < this.endNum; // true=递增,false=递减
      doMethods[this.increaseAni ? 'increase' : 'decrease'];
    },

    /**
     * @description: 递减操作
     * @param {*} start
     * @param {*} end
     * @return {*}
     */
    doDecreaseAni(start, end) {
      const difference = start - end;
      const intervalTime = this.duration / difference;
      console.log({'decrease', start, end, difference, intervalTime});

      this.decreaseTimer = setInterval(() => {
        if (start <= end) {
          clearInterval(this.decreaseTimer);
          this.decreaseTimer = null;
          return;
        }
        start--;
        this.nowNumArr = this.transformNumArr(start, this.numLength);
      }, intervalTime);
    },

    /**
     * @description: 递增操作
     * @param {*} start
     * @param {*} end
     * @return {*}
     */
    doIncreaseAni(start, end) {
      const difference = end - start;
      const intervalTime = this.duration / difference;
      console.log('increase', {start, end, difference, intervalTime});

      this.increaseTimer = setInterval(() => {
        if (start >= end) {
          clearInterval(this.increaseTimer);
          this.increaseTimer = null;
          return;
        }
        start++;
        this.nowNumArr = this.transformNumArr(start, this.numLength);
      }, intervalTime);
    },

    /**
     * @description: 将数字转成数组
     * @param {*} number
     * @param {*} length 数组长度,位数不足前补0,位数超出取最大值999...
     * @return {Array}
     */
    transformNumArr(number, length) {
      const newNumber = String(number).length <= length
        ? this.padZero(number, length, 'left')
        : this.padZero(1, length + 1, 'left') - 1;
      return String(newNumber).split('');
    },

    /**
     * @description: 字符串补0
     * @param {*} num
     * @param {*} len
     * @param {*} direction
     * @return {String}
     */
    padZero(num = 0, len = 0, direction = 'left') {
      return direction === 'left'
        ? (`${num}`).padStart(len, '0')
        : (`${num}`).padEnd(len, '0');
    },
  },
};
</script>

<style lang="scss" scoped>
// 圆角
$borderRadius: 0.1rem;
// 总数字盒子宽度
$allNumberWidth: 7.5rem;
// 单个数字盒子宽度
$singleNumberWidth: 1rem;
// 字体大小
$fontSize: 1.2rem;

// 数字翻转盒子
.digital-flip-box {
  font-size: $fontSize;
  width: $allNumberWidth;
  height: 1.5rem;
  line-height: 1;
  margin: 0 auto;
  font-weight: bold;
  color: #fedec2;
  background: gold;
  display: flex;
  justify-content: center;
}

// 单个数字盒子
.digital-single-box {
  position: relative;
  width: $allNumberWidth;
  height: 100%;
  border: 1px solid black;
  border-radius: $borderRadius;
  margin: 0 0.1rem;
}

// 数字列 0~9
.digital-single-list {
  position: absolute;
  width: 100%;
  height: 100%;

  div {
    position: absolute;
    left: 0;
    width: 100%;
    height: 50%;
    overflow: hidden;
  }

  // 顶部数字、数字阴影
  .digit-top, .shadow-top {
    background: #fedec2;
    border-bottom: 1px solid #fedec2;
    box-sizing: border-box;
    top: 0;
    z-index: 0;
    border-radius: $borderRadius $borderRadius 0 0;
    &::before {
      content: "";
      position: absolute;
      left: 0;
      top: 0;
      height: 100%;
      width: 100%;
      border-radius: $borderRadius $borderRadius 0 0;
    }
  }
  .shadow-top {
    opacity: 0;
    transition: opacity 0.3s ease-in;
  }

  // 底部数字、数字阴影
  .digit-bottom, .shadow-bottom {
    background: #fedec2;
    left: 0;
    bottom: 0;
    z-index: 0;
    border-radius: 0 0 $borderRadius $borderRadius;
    &::before {
      content: "";
      position: absolute;
      left: 0;
      top: 0;
      height: 100%;
      width: 100%;
      border-radius: 0 0 $borderRadius $borderRadius;
    }
  }
  .shadow-bottom {
    opacity: 0;
    transition: opacity 0.3s ease-in;
  }

  // 实际数字
  .digit-num {
    line-height: 1;
    display: block;
    overflow: hidden;
    display: flex;
    justify-content: center;
  }
  .digit-bottom, .shadow-bottom {
    .digit-num {
      margin-top: -75%;
    }
  }
}

// 动效
.digital-single-list {
  // 递减9~0
  &.pre {
    .digit-top, .shadow-top {
      opacity: 1;
      z-index: 2;
      transform-origin: 50% 100%;
      animation: flip-ani-1 0.3s ease-in both;
      @keyframes flip-ani-1 {
        0% {
          transform: perspective(400px) rotateX(0deg);
        }
        100% {
          transform: perspective(400px) rotateX(-90deg);
        }
      }
    }
    .digit-bottom, .shadow-bottom {
      opacity: 1;
      z-index: 1;
    }
  }

  // 递减9~0
  &.active-pre {
    .digit-top {
      z-index: 1;
    }
    .digit-bottom {
      z-index: 2;
      transform-origin: 50% 0%;
      animation: flip-ani-2 0.3s ease-out both;
      @keyframes flip-ani-2 {
        0% {
          transform: perspective(400px) rotateX(-90deg);
        }
        100% {
          transform: perspective(400px) rotateX(0deg);
        }
      }
    }
  }

  // 递增0~9
  &.active-next {
    .digit-top {
      z-index: 2;
      transform-origin: 50% 100%;
      animation: flip-ani-3 0.3s ease-out both;
      @keyframes flip-ani-3 {
        0% {
          transform: perspective(400px) rotateX(-90deg);
        }
        100% {
          transform: perspective(400px) rotateX(0deg);
        }
      }
    }
    .digit-bottom {
      z-index: 1;
    }
  }

  // 递增0~9
  &.next {
    .digit-top, .shadow-top {
      opacity: 1;
      z-index: 1;
    }
    .digit-bottom, .shadow-bottom {
      opacity: 1;
      z-index: 2;
      transform-origin: 50% 0%;
      animation: flip-ani-4 0.3s ease-in both;
      @keyframes flip-ani-4 {
        0% {
          transform: perspective(400px) rotateX(0deg);
        }
        100% {
          transform: perspective(400px) rotateX(90deg);
        }
      }
    }
  }
}
</style>