用于滚动数字的指令(超级详细)

本文最后更新于:2023年11月5日 晚上

指令需求

  1. 能自动识别并滚动目标元素上的所有能滚动的数字
  2. 支持自定义滚动数字的样式,如颜色,加粗
  3. 支持自定滚动的时间
  4. 滚动过程中不会导致整段文字宽度发生变化,如 1 - 1000000000不会导致整段跟着慢慢变宽

先看使用

使用代码展示:

1
2
3
4
import Vue from 'vue'
import scrollNum from "./scroll-num"

Vue.directive('scroll-num', scrollNum)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<div v-scroll-num="{
lightClass: 'light-num',
duration: 1000
}" class="demo">
正常数字1002,这是非数字1001.1.1,这是浮点数1.233444
</div>
<!-- 对比组 -->
<div class="demo">
正常数字1002,这是非数字1001.1.1,这是浮点数1.233444
</div>
</div>
</template>
<style lang="scss">
.demo {
font: 20px "Fira Sans";
}
.light-num {
color: red;
}
</style>

scroll-num.js

rAFWithFPS.js见上一篇:优雅的封装rAF

vue2 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import rAFWithFPS from "@/rAFWithFPS";

// 利用画布获取文字的渲染宽度
const getRenderWidthOfText = (text, font = '') => {
// 创建一个画布元素
const canvas = document.createElement('canvas')
// 获取渲染上下文对象
const ctx = canvas.getContext('2d')
// 设置文字属性
ctx.font = font
// 获取测量文本数据中的宽度
return ctx.measureText(text).width
}

// 利用曲线缓动元素的值同时控制刷新率
const runAttr = ({ targetNode, scrollNum, duration, easing }) => {
let fixed = 0
const strNum = scrollNum + ''
// 获取数字的小数位下标
const val = strNum.lastIndexOf('.')
if (val !== -1) fixed = strNum.length - (val + 1)
// 利用曲线累加
rAFWithFPS(progress => {
targetNode.innerHTML = (easing(progress) * scrollNum).toFixed(fixed)
}, duration)
}
export default {
bind (el, {
value: {
lightClass = '',
easing = x => Math.sin((x * Math.PI) / 2),
duration = 500
} = {}
}, vNode) {
// 替换后的span默认样式
const defaultStyle = {
display: 'inline-block',
'text-align': 'center',
'box-sizing': 'content-box'
}
// 渲染完成后计算
vNode.context.$nextTick(() => {
// 获取文字大小和字体
const font = getComputedStyle(el).font
// 先匹配由数字组成的字符
const replaceStr = el.innerText.replaceAll(/[\d\.]+/g, $1 => {
// 转换为数字失败就返回
if (isNaN(+$1)) return $1
// 获取渲染宽度
const spanWidth = Math.floor(getRenderWidthOfText($1.trim(), font))
// style对象转为对象字符串,不然渲染不出来
const style = {
...defaultStyle,
// font: `${ font.replaceAll(/["]/g, '') }`,
width: `${ spanWidth }px`
}
let styleStr = Object
.entries(style)
.reduce((result, [ key, value ]) => result + `${ key }:${ value };`, '')
// 将正常数字替换为span标签,这里必须不留换行,不然span会出现空隙
return `<span
scroll-num="${ $1 }"
class="${ lightClass }"
style="${ styleStr }">
</span>`
})
el.innerHTML = replaceStr || '&nbsp;'
// 获取所有span
const allLightNode = el.querySelectorAll('span[scroll-num]')
for (const node of allLightNode) {
const scrollNum = +node.getAttribute('scroll-num')
runAttr({
targetNode: node,
scrollNum,
easing,
duration,
})
}
})
}
}

vue3 版本

  1. bind生命周期换成mounted
  2. vNode.context.$nextTick改为使用外部引入的nextTick
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import rAFWithFPS from "@/rAFWithFPS";
import { nextTick } from "vue";

// 利用画布获取文字的渲染宽度
const getRenderWidthOfText = (text, font = '') => {
// 创建一个画布元素
const canvas = document.createElement('canvas')
// 获取渲染上下文对象
const ctx = canvas.getContext('2d')
// 设置文字属性
ctx.font = font
// 获取测量文本数据中的宽度
return ctx.measureText(text).width
}

// 利用曲线缓动元素的值同时控制刷新率
const runAttr = ({ targetNode, scrollNum, duration, easing }) => {
let fixed = 0
const strNum = scrollNum + ''
// 获取数字的小数位下标
const val = strNum.lastIndexOf('.')
if (val !== -1) fixed = strNum.length - (val + 1)
// 利用曲线累加
rAFWithFPS(progress => {
targetNode.innerHTML = (easing(progress) * scrollNum).toFixed(fixed)
}, duration)
}
export default {
mounted (el, {
value: {
lightClass = '',
easing = x => Math.sin((x * Math.PI) / 2),
duration = 500
} = {}
}) {
// 替换后的span默认样式
const defaultStyle = {
display: 'inline-block',
'text-align': 'center',
'box-sizing': 'content-box'
}
// 渲染完成后计算
nextTick(() => {
// 获取文字大小和字体
const font = getComputedStyle(el).font
// 先匹配由数字组成的字符
const replaceStr = el.innerText.replaceAll(/[\d\.]+/g, $1 => {
// 转换为数字失败就返回
if (isNaN(+$1)) return $1
// 获取渲染宽度
const spanWidth = Math.floor(getRenderWidthOfText($1.trim(), font))
// style对象转为对象字符串,不然渲染不出来
const style = {
...defaultStyle,
// font: `${ font.replaceAll(/["]/g, '') }`,
width: `${ spanWidth }px`
}
let styleStr = Object
.entries(style)
.reduce((result, [ key, value ]) => result + `${ key }:${ value };`, '')
// 将正常数字替换为span标签,这里必须不留换行,不然span会出现空隙
return `<span
scroll-num="${ $1 }"
class="${ lightClass }"
style="${ styleStr }">
</span>`
})
el.innerHTML = replaceStr || '&nbsp;'
// 获取所有span
const allLightNode = el.querySelectorAll('span[scroll-num]')
for (const node of allLightNode) {
const scrollNum = +node.getAttribute('scroll-num')
runAttr({
targetNode: node,
scrollNum,
easing,
duration,
})
}
})
}
}

用于滚动数字的指令(超级详细)
https://xin-fas.github.io/2023/08/26/用于滚动数字的指令(超级详细)/
作者
Xin-FAS
发布于
2023年8月26日
更新于
2023年11月5日
许可协议