奇葩的数字大屏液晶数字效果

185 阅读3分钟

最近接收到一个大屏需求,需求是酱紫的,直接上图

flipper.gif

需求拆解

  1. 液晶数字滚动效果和特定的字体。
  2. 有从旧值过渡到新值的效果,所以需要知道oldValue和newValue两个值。
  3. 如果新旧值相差不大的话,产生的随机数组的个数

首先实现液晶数字组件

template部分

<template>
  <div class="flipper-container">
    <div class="flipper-item" v-for="item in state.flipperData">
      <div class="flipper-item-list" :data-number="item">
        <div
          class="flipper-item__inner"
          v-for="number in numbers"
          :key="number"
        >
          {{ number }}
        </div>
      </div>
    </div>

    <div class="animation-list">
      <div
        class="animation-item"
        :class="isIncreasing ? 'green' : 'red'"
        v-for="number in state.differenceData"
      >
        <span class="mark">
          {{ isIncreasing ? '+' : '-' }}
        </span>
        <span class="number">{{ number }}</span>
      </div>
    </div>
  </div>
</template>
  • 导入液晶字体文件
  • 首先每一个数字都是由一组0-9的数字排列组成,然后根据当前数字的值,使用translateY滚动到相应的位置,以达到显示当前数字的效果。
  • animation-list中包裹的则是随机生成的数字,用于右侧相加动效

核心函数

首先我们需要一个方法把number转为Array,类似 1234 -> [1, 2, 3, 4],我们最终渲染页面的时候,是要for循环一个数组

/**
 * @description number -> Array<number>
 * @param {Number}  data
 * @return {Array<number>}
 */
const getFlipperData = (data: number = 0) => {
  return data
    .toString()
    .padStart(state.flipperLength, '0') // 左侧不够长度的 填充0
    .split('') // 转数组
    .map(item => stringToNumber(item)) // 数组内转number
}

我们还需要一个辅助函数,用于生成oldValue到newValue之间的随机数组

/**
 * @description 获取差值的随机数数组
 * @param {Number}  difference  差值
 * @returns {Array<Number>}
 */
const getDifferenceData = (difference: number) => {
  if (difference === 0) return []
  let result: number[] = []
  let sum = 0
  for (let i = 0; i < state.differenceGroupNumber - 1; i++) {
    let randomNum = Math.floor(
      Math.random() * (difference - sum - (state.differenceGroupNumber - i)) + 1
    )
    result.push(randomNum)
    sum += randomNum
  }
  result.push(stringToNumber((difference - sum).toFixed(1)))
  return result
}

script代码块

<script setup lang="ts">
import { computed, onMounted, reactive } from 'vue'

interface Data {
  oldValue: number
  newValue: number
}

interface Props {
  data: Data
}

interface State {
  flipperData: number[]
  flipperLength: number
  difference: number
  differenceData: number[]
  differenceGroupNumber: number
}

const props = withDefaults(defineProps<Props>(), {})

const numbers = Object.freeze([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

// 默认的分组数
const DEFAULT_GROUP = 5

// 默认的动画时间
const ANIMATE_TIME = 1000

const state = reactive<State>({
  // 当前的flipperData
  flipperData: [],
  // 数字长度
  flipperLength: 0,
  // 差值
  difference: 0,
  // 差值数组
  differenceData: [],
  // 分组数量
  differenceGroupNumber: 0,
})

// 默认样式配置,灵活的话,也可由props传入
const styleInfo = reactive({
  width: 65,
  height: 90,
  fontSize: 80,
})

// 是否是递增
const isIncreasing = computed(() => state.difference > 0)

/**
 * @description 延迟函数
 * @param {Number}  timer  毫秒
 * @returns {Promise<any>}
 */
const sleep = (time: number) => {
  return new Promise(resolve => setTimeout(resolve, time))
}

/**
 * @description 字符串转数字
 * @param {String}  str
 * @returns {Number}
 */
const stringToNumber = (str: string) => +str

/**
 * @description number -> Array<number>
 * @param {Number}  data
 * @return {Array<number>}
 */
const getFlipperData = (data: number = 0) => {
  return data
    .toString()
    .padStart(state.flipperLength, '0') // 左侧不够长度的 填充0
    .split('') // 转数组
    .map(item => stringToNumber(item)) // 数组内转number
}

/**
 * @description 获取分组数量
 * @param {Number}  difference  差值
 * @returns {Array<number>}
 */
const getDifferenceGroupNumber = (difference: number) => {
  difference = Math.abs(difference)
  if (difference <= 1) {
    return 1
  } else if (difference <= 5) {
    return 2
  } else if (difference <= 10) {
    return 3
  } else {
    return DEFAULT_GROUP
  }
}

/**
 * @description 获取差值的随机数数组
 * @param {Number}  difference  差值
 * @returns {Array<Number>}
 */
const getDifferenceData = (difference: number) => {
  if (difference === 0) return []
  let result: number[] = []
  let sum = 0
  for (let i = 0; i < state.differenceGroupNumber - 1; i++) {
    let randomNum = Math.floor(
      Math.random() * (difference - sum - (state.differenceGroupNumber - i)) + 1
    )
    result.push(randomNum)
    sum += randomNum
  }
  result.push(stringToNumber((difference - sum).toFixed(1)))
  return result
}

// 更新动画
const updateStyle = () => {
  const flipperListEle = document.querySelectorAll('.flipper-item-list')
  flipperListEle.forEach((ele: HTMLElement) => {
    const number = ele.dataset.number ?? 0
    ele.style.transform = `translate3d(0, -${styleInfo.height * +number}px, 0)`
  })
}

/**
 * @description 循环 生成随机数组,用oldValue累加更新 flipperData
 */
const updateFlipperData = async () => {
  let value = props.data.oldValue
  for (const number of state.differenceData) {
    value += number
    value.toString().padStart(state.flipperLength, '0')
    state.flipperData = getFlipperData(value)
    console.log('updateFlipperData', state.flipperData)
    await sleep(ANIMATE_TIME).then(updateStyle)
  }
}

const initData = async () => {
  state.flipperLength = props.data.newValue.toString().length
  state.flipperData = getFlipperData(props.data.oldValue)
  // 获取差值
  state.difference = props.data.newValue - props.data.oldValue
  // 根据差值的大小,生成分组个数
  state.differenceGroupNumber = getDifferenceGroupNumber(state.difference)

  // 更具分组个数,生成随机数组
  state.differenceData = getDifferenceData(state.difference)

  // TODO: 首次更新动画 等待dom更新完毕
  await sleep(ANIMATE_TIME).then(updateStyle)
}

const init = () => {
  initData()
  updateFlipperData()
}

onMounted(init)
</script>

style的话就随便写写,不是很擅长,主要的就是 @keyframes 的动画,可以根据需求来改写动画

<style scoped lang="less">
// 随机数
@random: `Math.floor(Math.random() * 6) - 3`;
// 随机缩放
@scale: `Math.random() * 1.8`;
// 角度随机
@degrees: unit(@random, deg);
// 分组个数
@differenceGroupNumber: v-bind('state.differenceGroupNumber');

.flipper-container {
  position: relative;
  display: flex;
  width: 100%;
  height: 100%;
  align-items: flex-end;

  .flipper-item {
    position: relative;
    width: v-bind('`${styleInfo.width}px`');
    height: v-bind('`${styleInfo.height}px`');
    line-height: v-bind('`${styleInfo.height}px`');
    padding-top: 10px;
    margin-right: 3%;
    font-family: 'Segment7';
    font-size: v-bind('`${styleInfo.fontSize}px`');
    text-align: center;
    color: #ffffff;
    border-width: 2px;
    border-style: solid;
    border-color: #fff;
    border-radius: 4px;
    background-image: linear-gradient(45deg, #30374d, #485573);
    overflow: hidden;
    &-list {
      height: v-bind('`${styleInfo.height * 10}px`');
      transition: all 1s ease;
      background-color: transparent;
    }
    &__inner {
      width: v-bind('`${styleInfo.width}px`');
      height: v-bind('`${styleInfo.height}px`');
      background-color: transparent;
    }

    &:before {
      content: '';
      position: absolute;
      top: 0;
      width: 100%;
      height: 100%;
      background: linear-gradient(
        -70deg,
        transparent 45%,
        rgba(0, 0, 0, 0.17) 46%
      );
      left: 50%;
      transform: translate(-50%);
    }

    &:after {
      content: '8';
      position: absolute;
      top: 10px;
      left: 0;
      right: 0;
      bottom: 0;
      color: rgba(106, 139, 255, 0.2);
      font-family: 'Segment7';
    }
    &:last-child {
      margin-right: 0;
    }
  }

  @keyframes fadeIn {
    0% {
      transform: translateY(-50px) rotate(@degrees) scale(0.5);
      opacity: 0;
    }
    25% {
      transform: translateY(-60px) rotate(@degrees) scale(1.3);
      opacity: 0.8;
    }
    50% {
      transform: translateY(-70px) rotate(@degrees) scale(1);
      opacity: 1;
    }
    75% {
      transform: translateY(-80px) rotate(@degrees) scale(0.8);
      opacity: 0.6;
    }
    100% {
      transform: translateY(-100px) rotate(0) scale(0.6);
      opacity: 0;
    }
  }

  .animation-loops(@i) when (@i <= 5) {
    &:nth-of-type(@{i}) {
      transform: rotate(@degrees) scale(@scale);
      animation: fadeIn 1s @i * 1s forwards;
    }

    .animation-loops(@i + 1);
  }

  .animation-list {
    font-size: 40px;

    .animation-item {
      position: absolute;
      bottom: 20px;
      right: 0;
      opacity: 0;
      min-width: 55px !important;
      text-align: center;
      .number {
        font-family: 'Segment7';
      }
      &.green {
        color: #52c41a;
      }
      &.red {
        color: #ff4d4f;
      }
      .animation-loops(1);
    }
  }
}
</style>

虽然混迹掘金好多年了,还是第一次想起写文章,排版好🌶🐔,不会不会写,仅仅记录一下!!