如何让CSS计数器支持小数的动态变化?

1,367 阅读8分钟

欢迎关注我的公众号:前端侦探

不得不说,CSS 计数器是个好东西。

最近在几篇文章中都用到了 CSS 计数器,可以将 CSS 变量通过伪元素content动态展示出来,还可以做出很多有趣的动画。有兴趣的可以先回顾一下之前的这几篇文章:

原理其实很简单,content虽然本身不支持 CSS 变量直接渲染,但是可以支持counter-reset

count::before {
  	--percent: 50;
    counter-reset: progress var(--percent);
    content: counter(progress);
}

通过一次中转,就可以让content也能支持CSS变量作为字符展示了

image.png

这个技巧是通过张鑫旭的这篇文章了解的,非常实用:小tips: 如何借助content属性显示CSS var变量值

但是,这个方法有个比较遗憾的地方就是,CSS 计数器不支持真正意义上的小数,也就是如果 CSS 变量为小数的话,直接展示为 0

count::before {
  	--percent: 50.15;
    counter-reset: progress var(--percent);
    content: counter(progress);
}

image.png

那么,如何让content也支持CSS变量的小数展示呢,毕竟很多情况下还是需要小数的?比如下面这个,如果支持了小数,就可以轻易的实现数字的滚动动画

Kapture 2022-08-27 at 14.25.44.gif

今天一起来探讨一下

一、CSS 原理拆解

CSS 计数器由于特殊性,目前都是仅支持整数的,毕竟自然个数是没有小数的(不排除以后自定义计数器可以实现)。既然这样,可以换一种思路,从数字形态上进行拆分。比如一个小数,48.69可以分解成整数部分48和小数部分69,然后再通过小数点链接起来。这样拆分后就都是整数了, CSS 计数器也是支持的

image-20220827131936587

用代码实现就是(便于理解,以下的一些变量都是中文命名的,实际生产不推荐)

count::before {
  --整数: 48;
  --小数: 69;
  counter-reset: 整数计数器 var(--整数) 小数计数器 var(--小数);
  content: counter(整数计数器) "." counter(小数计数器);
}

image.png

所以问题就变成了,如何将一个小数进行拆分呢?

二、CSS变量拆分成整数和小数

接着上面的问题,假设变量是--percent,问题就是下面两个变量--整数--小数如何通过--percent计算而来呢?

count::before {
  --percent: 48.69;
  --整数: 48;
  --小数: 69;
  counter-reset: 整数计数器 var(--整数) 小数计数器 var(--小数);
  content: counter(整数计数器) "." counter(小数计数器);
}

看似很容易,但在 CSS 中好像并不怎么好实现。

为了解决这个,需要了解一下 CSS 自定义变量的类型。类型有很多,下面罗列一下

  • <length>
  • <number>
  • <percentage>
  • <length-percentage>
  • <color>
  • <image>
  • <url>
  • <integer>
  • <angle>
  • <time>
  • <resolution>
  • <transform-function>
  • <custom-ident>
  • <transform-list>

大部分能可以看出具体的类型,我们这里需要用到的就两种,<number><integer>,两者都表示数字,具体的区别在于

  • <number>表示任意的数字,整数和小数都可以
  • <integer>表示整型数字,只能是整数,小数会认为不合法

回到这里,默认情况下,CSS 变量可以是任意值,但是通过自定义变量@property可以指定变量的类型,它可以对不合法的变量进行转换。

@property - CSS(层叠样式表) | MDN (mozilla.org)

比如,我们需要一个整数,可以这样来定义,将syntax属性设置为<integer>就可以了

@property --整数 {
  syntax: "<integer>"; /*整型*/
  initial-value: 0;
  inherits: false;
}

这样,这个变量会被强制转换成整数。比如,下面给--整数也设置成一个小数

count::before {
  --percent: 48.69;
  --整数: 48.69;
  --小数: 69;
  counter-reset: 整数 var(--整数) 小数 var(--小数);
  content: counter(整数) "." counter(小数);
}

结果...

image.png

居然直接变成了 0

不过没关系,需要可以配合一些 CSS 计算函数实现自动转换,比如calc

count::before {
  --percent: 48.69;
  --整数: calc(48.69);/*使用 CSS 计算后可以转换成整数*/
  --小数: 69;
  counter-reset: 整数 var(--整数) 小数 var(--小数);
  content: counter(整数) "." counter(小数);
}

image.png

但是,这里变成了49,原因其实是四舍五入造成的,并不是向下取整。为了消除这种误差,可以再减去0.5,所以整数部分的最终实现就是

@property --整数 {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}
count::before {
  --percent: 48.69;
  --整数: calc(var(--percent) - 0.5);
  --小数: 69;
  counter-reset: 整数 var(--整数) 小数 var(--小数);
  content: counter(整数) "." counter(小数);
}

未来的 CSS 数学函数应该也会有 floor、ceil 这样的,可以期待一下~

然后是小数部分,有了整数部分,小数部分就容易了,可以用整个值减去整数部分,然后乘以 100,示意如下

image.png

用代码实现就是

@property --小数 {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}
count::before {
  --percent: 48.69;
  --整数: calc(var(--percent) - 0.5);
  --小数: calc((var(--percent) - var(--整数)) * 100 - 0.5);
  counter-reset: 整数 var(--整数) 小数 var(--小数);
  content: counter(整数) "." counter(小数);
}

效果如下

image.png

后面最末位的小数由于四舍五入的关系稍微有些偏差,没关系,可以修正一下,加上0.01就行了。其次,还有一个问题,当小数位小于 10 的时候,计算出的结果可能是这样

image.png

那么,这种情况就需要动态补零了。

关于“补零”的技巧,之前在这篇文章中有过详细介绍:CSS 字符串补全?

所以,只需要在计数器后面定义一下计数器样式decimal-leading-zero,表示十进制前置零,最终实现如下

count::before {
  --percent: 48.69;
  --整数: calc(var(--percent) - 0.5);
  --小数: calc((var(--percent) - var(--整数)) * 100 - 0.5 + 0.01);
  counter-reset: 整数 var(--整数) 小数 var(--小数);
  content: counter(整数) "." counter(小数, decimal-leading-zero);
}

这样整数和小数都可以用同一个变量--percent表示出来了,完美~

三、CSS 变量动画

有人可能会觉得,为啥要废这么大劲去实现这样一个功能?用 js 直接设置不行吗?如果仅仅是数字的变化,那当然可以,但在这里,除了CSS 单一变量带来更好的可维护性外, 还可以做到连 JS 也难以做到(或者说成本更高)的事情,比如过渡动画

首先,再改进一下,很多小数都是百分比形式的,也就是0~1范围内,所以前面--percent可能是这样的值0.4869

count::before {
  --percent: 0.4869;
  --百分比: calc(var(--percent) * 100);
  --整数: calc(var(--百分比) - 0.5);
  --小数: calc((var(--百分比) - var(--整数)) * 100 - 0.5 + 0.01);
  counter-reset: 整数 var(--整数) 小数 var(--小数);
  content: counter(整数) "." counter(小数, decimal-leading-zero) "%";
}

效果如下

image.png

然后,我们通过 JS 让这个数字随机变化

count.addEventListener('click', ev => {
  ev.target.style.setProperty("--percent", Math.random());
})

效果如下

Kapture 2022-08-27 at 14.14.47.gif

但是,这样太死板了,我们需要数字变化的时候有个动画,可以直接通过 CSS 自定义变量实现

@property --percent {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}
count{
  /**/
  transition: --percent 1s
}

现在看看效果,非常轻松的就实现了数字的滚动动画

640 (1).gif

小数部分由于是跟随整数部分的,比如整数从1变为3,那么小数部分就跟随变化两个循环。本来这个也非常符合常理,就像时钟的秒永远要比分要转的快一样,但是有人可能觉得变的太快了,有没有办法让小数部分和整数部分独立开来呢?当然也是可以的,而且非常容易,只需要给整数部分和小数部分分别设置过渡就行了

count{
  /**/
  transition: --整数 1s, --小数 1s;
}

现在再看看效果,和上面对比一下

Kapture 2022-08-27 at 14.25.44.gif

这两种效果可以自行选择,仅仅只是过渡的不同

试想一下,如果这个效果用 JS 来实现,是不是还有点点麻烦呢?

下面是完整代码(不多,就这么几行)

@property --percent {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}
@property --整数 {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}
@property --小数 {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}
count {
  --percent: 0.4512;
  font-size: 60px;
  font-weight: bolder;
  cursor: pointer;
  font-family: 'Courier New', Courier, monospace;
  --百分比: calc(var(--percent) * 100);
  --整数: calc(var(--百分比) - 0.5);
  --小数: calc((var(--百分比) - var(--整数)) * 100 - 0.5 + 0.01);
  counter-reset: 整数 var(--整数) 小数 var(--小数);
  transition: --整数 1s, --小数 1s;
}
count::before {
  content: counter(整数) "." counter(小数, decimal-leading-zero) "%";
}

你也可以访问线上 demo: CSS double num(codepen.io) 或者 CSS double num(runjs.work) 或者 CSS double num (juejin.cn)

四、总结和说明

以上就是全部内容了,一个还不错的小技巧,你学会了吗?

  1. CSS 变量不支持直接在content中渲染,但是可以借助计数器初始化来实现
  2. CSS 计数器不支持小数初始化
  3. CSS 计数器支持小数的实现原理在于将小数拆分为整数、小数点、小数三个部分
  4. CSS 自定义变量可以指定变量的类型,这样通过 CSS 数学函数可以将一个小数转换成整数
  5. 小数部分可以通过减去整数部分得到
  6. 小数部分还需要通过decimal-leading-zero补全位数
  7. CSS 单一变量一方面可以带来更好的可维护性,另一方面还可以更轻易地实现过渡动画
  8. 借助 @property可以很方便的控制 CSS 变量的过渡和动画

数字变化动画在一些数据大屏展示的场景下还是挺实用的,有了 CSS 变量,再也不需要通过 JS 去实时计算了。不过目前兼容性还不是太好,适合内部项目小范围使用(当然直接用了不要紧,不支持的只是没有动画而已)。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发❤❤❤

欢迎关注我的公众号:前端侦探