查看原文
其他

【第2729期】如何让CSS计数器支持小数的动态变化?

XboxYan 前端早读课 2022-09-14

前言

拆解的思路,找回了当年学习的劲头。今日前端早读课文章由阅文 @XboxYan 分享,公号:前端侦探授权。

@XboxYan,阅文体验设计部,专注用户体验相关,热爱 CSS,热爱原生,欢迎关注 github:https://github.com/XboxYan

正文从这开始~~

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

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

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

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

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

这个技巧是通过这篇文章了解的,非常实用:

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

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

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


今天一起来探讨一下

一、CSS 原理拆解

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

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

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

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

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

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

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

看似很容易,但在 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 可以指定变量的类型,它可以对不合法的变量进行转换。

比如,我们需要一个整数,可以这样来定义,将 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(小数);
}

结果...

居然直接变成了 0?

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

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

但是,这里变成了 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,示意如下

用代码实现就是

@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(小数);
}

效果如下

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

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

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

所以,只需要在计数器后面定义一下计数器样式 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) "%";
}

效果如下

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

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

效果如下


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

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

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

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

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

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


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

试想一下,如果这个效果用 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 变量不支持直接在 content 中渲染,但是可以借助计数器初始化来实现

  • CSS 计数器不支持小数初始化

  • CSS 计数器支持小数的实现原理在于将小数拆分为整数、小数点、小数三个部分

  • CSS 自定义变量可以指定变量的类型,这样通过 CSS 数学函数可以将一个小数转换成整数

  • 小数部分可以通过减去整数部分得到

  • 小数部分还需要通过 decimal-leading-zero 补全位数

  • CSS 单一变量一方面可以带来更好的可维护性,另一方面还可以更轻易地实现过渡动画

  • 借助 @property 可以很方便的控制 CSS 变量的过渡和动画

数字变化动画在一些数据大屏展示的场景下还是挺实用的,有了 CSS 变量,再也不需要通过 JS 去实时计算了。不过目前兼容性还不是太好,适合内部项目小范围使用(当然直接用了不要紧,不支持的只是没有动画而已)。

关于本文
作者:@XboxYan
原文:https://mp.weixin.qq.com/s/MAiofiJSTM9SbC2T28WhBQ

关于【CSS】相关推荐,欢迎读者自荐投稿,前端早读课等你

【第2711期】利用CSS实现超长内容滚动播放

【第2684期】CSS Houdini 实现磁吸效果

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存