1. gsap数字滚动插件
GSAP的全名是GreenSock Animation Platform,是一套脚本动画工具。
<template>
<div class="number-roll">{{ showCounter }}</div>
</template>
<script>
import gsap from 'gsap'
export default {
name: 'NumberRoll',
props: {
number: {
type: [Number, String],
default: 0
}
},
data() {
return {
showNumber: 0
}
},
computed: {
showCounter() {
return this.showNumber.toFixed(0)
}
},
mounted() {
gsap.to(this, {
duration: 1,
showNumber: this.number
})
},
watch: {
number(newVal) {
gsap.to(this, {
duration: 1,
showNumber: newVal
})
}
}
}
</script>
2. Vue手写好看的滚动数字插件
大屏数字滚动翻转效果来源于最近工作中element后台管理页面一张大屏的UI图,该UI图上有一个模块需要有数字往上翻动的效果,以下是最终实现的效果:
整体思路:
在实现此效果之前,我们先来捋一下思路,用思维导图来设计一下我们的实现步骤,如下:
你可以审查元素,下载数字背景图片,复制图片地址,或者使用其他背景图片、背景颜色
有了以上的设计流程,我们先来简单实现一下:
// CSS代码
<style>
.box-item {
position: relative;
display: inline-block;
width: 54px;
height: 82px;
/* 背景图片 */
background: url(./number-bg.png) no-repeat center center;
background-size: 100% 100%;
font-size: 62px;
line-height: 82px;
text-align: center;
}
</style>
// htm代码
<div class="box">
<p class="box-item">
<span>1</span>
</p>
</div>
实现以上代码后,它的效果将是下面这样的:
思考:
背景框中有了数字以后,我们现在来思考一下,背景框中的文字,一定是0-9之前的数字,要在不打乱以上html结构的前提下,如何让数字滚动起来呢?这个时候我们的魔爪就伸向了一个CSS属性:writing-mode
,下面是它属性的介绍:
horizontal-tb
:默认值,表示水平排版,从上到下。vertical-lr
:表示垂直排版,从左到右。vertical-rl
:表示垂直排版,从右到左。
它的实时效果是像下面这样:
根据以上的灵感,我们可以实现下面这样的效果:
代码如下:
// html部分
<p class="box-item">
<span>0123456789</span>
</p>
// style部分
.box-item {
display: inline-block;
width: 54px;
height: 82px;
background: url(./number-bg.png) no-repeat center center;
background-size: 100% 100%;
font-size: 62px;
line-height: 82px;
text-align: center;
position: relative;
writing-mode: vertical-lr;
text-orientation: upright;
/* overflow: hidden; */
}
.box-item span {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
letter-spacing: 10px;
}
计算滚动:
如果我们想让数字滚动到5,那么滚动的具体到底是多少?
答案是:向下滚动-50%
那么其他的数字呢?
得益于我们特殊的实现方法,每一位数字的滚动距离有一个通用的公式:
transform: `translate(-50%,-${number * 10}%)
滚动动画的实现
在知道了每一个数字具体的滚动距离后,我们来设计一下,让数字能够随机滚动起来:
setInterval(() => {
let number = document.getElementById('Number')
let random = getRandomNumber(0,10)
number.style.transform = `translate(-50%, -${random * 10}%)`
}, 2000)
function getRandomNumber (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
对不足八位进行补位
完善
在上一节中,我们初步完成了滚动的效果,这一节我们将根据最开始的思维导图来设计一个通用的Vue业务组件
因为我们的业务需要,我们最大的位数是8位数字,所以对不足八位进行补位:
假如传递的位数不足8位,我们需要对它进行补0的操作,补0完成以后,我们也需要把它转换成金额的格式
toOrderNum(num) {
num = num.toString()
// 把订单数变成字符串
if (num.length < 8) {
num = '0' + num // 如未满八位数,添加"0"补位
this.toOrderNum(num) // 递归添加"0"补位
} else if (num.length === 8) {
// 订单数中加入逗号
num = num.slice(0, 2) + ',' + num.slice(2, 5) + ',' + num.slice(5, 8)
this.orderNum = num.split('') // 将其便变成数据,渲染至滚动数组
} else {
// 订单总量数字超过八位显示异常
this.$message.warning('订单总量数字过大,显示异常,请联系客服')
}
},
渲染
渲染
我们根据上面补位字符串,分隔成字符数组,在页面中进行渲染:
computeNumber:为字符数组,例如:['0','0',',','0','0','0',',','9','1','7']
// html代码
<ul>
<li
:class="{'number-item': !isNaN(item) }"
v-for="(item,index) in computeNumber"
:key="index"
>
<span v-if="!isNaN(item)">
<i ref="numberItem">0123456789</i>
</span>
<span v-else>{{item}}</span>
</li>
</ul>
// CSS代码
.number-item {
width: 50px;
background: url(./number-bg.png) no-repeat center center;
background-size:100% 100%;
& > span {
position: relative;
display: inline-block;
margin-right: 10px;
width: 100%;
height: 100%;
writing-mode: vertical-rl;
text-orientation: upright;
overflow: hidden;
& > i {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%,0);
transition: transform 0.5s ease-in-out;
letter-spacing: 10px;
}
}
}
页面渲染效果:
数字随机增长,模拟轮询效果
页面渲染完毕后,我们来让数字滚动起来,设计如下两个方法,其中increaseNumber需要在Vue生命周期mounted函数中调用
// 定时增长数字
increaseNumber () {
let self = this
this.timer = setInterval(() => {
self.newNumber = self.newNumber + getRandomNumber(1, 100)
self.setNumberTransform()
}, 3000)
},
// 设置每一位数字的偏移
setNumberTransform () {
let numberItems = this.$refs.numberItem
let numberArr = this.computeNumber.filter(item => !isNaN(item))
for (let index = 0; index < numberItems.length; index++) {
let elem = numberItems[index]
elem.style.transform = `translate(-50%, -${numberArr[index] * 10}%)`
}
}
最终实现效果:
完整代码如下:
<template>
<div class="number-roll">
<ul class="number-box">
<li
v-for="(item, index) in numArr"
:key="index"
:class="{ 'number-item': !isNaN(item), 'mark-item': isNaN(item) }"
>
<span v-if="!isNaN(item)">
<p
:style="{
transform: `translate(-50%, -${Number(numArr[index]) * 10}%)`
}"
>
0123456789
</p>
</span>
<span class="comma" v-else>{{ item }}</span>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, nextTick, watch } from "vue";
export default defineComponent({
name: "NumberRoll",
props: {
number: {
type: Number,
default: 0
}
},
setup(props:any) {
const numArr = ref<string[]>([]);
const init = (number: number) => {
// 如果不是小数
if (number && !String(number).includes(".")) {
let n = String(number)
.split("")
.reverse();
const len = n.length;
switch (true) {
case len > 0 && len <= 3:
numArr.value = n.reverse();
break;
case len > 3 && len <= 6:
numArr.value = (
n.slice(0, 3).join("") +
"," +
n.slice(3, 6).join("")
)
.split("")
.reverse();
break;
case len > 6 && len <= 9:
numArr.value = (
n.slice(0, 2).join("") +
"," +
n.slice(2, 5).join("") +
"," +
n.slice(5, 8).join("")
)
.split("")
.reverse();
break;
}
} else if (number && String(number).includes(".")) {
// 如果包含小数
numArr.value = String(number).split("");
}
};
nextTick(() => {
const n = String(props.number);
numArr.value = new Array(n.length).fill("");
setTimeout(() => {
init(props.number);
}, 4);
});
watch(
() => props.number,
(newVal, oldVal) => {
init(newVal);
}
);
return {
numArr
};
}
});
</script>
<style lang="scss" scoped>
/*FireFox*/
// @-moz-document url-prefix() {
// .number-roll .number-item {
// padding-top: 4px;
// }
// }
.number-roll {
position: relative;
.number-box {
display: flex;
justify-content: center;
align-items: center;
margin: 0 5px;
.number-item {
width: 36px;
height: 44px;
border-radius: 4px;
// margin: 0 2px;
background: url("./img/number-bg.png") no-repeat center center;
writing-mode: vertical-lr;
text-orientation: upright;
user-select: none;
overflow: hidden;
font-size: 0;
span {
position: relative;
display: block;
width: 100%;
height: 100%;
font-size: 0;
p {
position: absolute;
overflow: hidden;
left: 50%;
transform: translate(-50%, 0);
top: 0;
transition: transform 1.5s ease-in-out;
letter-spacing: 10px;
font-style: initial;
color: #fff;
font-family: "AgencyFB";
font-weight: 700;
font-size: 40px;
}
}
}
.mark-item {
width: 10px;
height: 44px;
margin-right: 5px;
line-height: 10px;
font-size: 48px;
position: relative;
top: 10px;
& > span {
position: absolute;
width: 100%;
bottom: 0;
writing-mode: vertical-rl;
text-orientation: upright;
color: #fff;
font-family: 'AgencyFB';
}
}
}
}
@-moz-document url-prefix() {
.number-item {
p {
margin-top: 4px;
}
}
}
</style>