如何讓CSS計數器支援小數的動態變化?

XboxYan 發表於 2022-11-24
CSS
歡迎關注我的公眾號:前端偵探

不得不說,CSS 計數器是個好東西。

最近在幾篇文章中都用到了 CSS 計數器,可以將 CSS 變數透過偽元素content動態展示出來,還可以做出很多有趣的動畫。有興趣的可以先回顧一下之前的這幾篇文章:

原理其實很簡單,content雖然本身不支援 CSS 變數直接渲染,但是可以支援counter-reset

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

透過一次中轉,就可以讓content也能支援CSS變數作為字元展示了

image-20220827130618618

這個技巧是透過張鑫旭的這篇文章瞭解的,非常實用:小tips: 如何藉助content屬性顯示CSS var變數值

但是,這個方法有個比較遺憾的地方就是,CSS 計數器不支援真正意義上的小數,也就是如果 CSS 變數為小數的話,直接展示為 0

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

image-20220827130753591

那麼,如何讓content也支援CSS變數的小數展示呢,畢竟很多情況下還是需要小數的?比如下面這個,如果支援了小數,就可以輕易的實現數字的滾動動畫

Kapture 2022-08-27 at 14.25.44

今天一起來探討一下

一、CSS 原理拆解

CSS 計數器由於特殊性,目前都是僅支援整數的,畢竟自然個數是沒有小數的(不排除以後自定義計數器可以實現)。既然這樣,可以換一種思路,從數字形態上進行拆分。比如一個小數,48.69可以分解成整數部分48和小數部分69,然後再透過小數點連結起來。這樣拆分後就都是整數了, CSS 計數器也是支援的

image-20220827131936587

用程式碼實現就是(便於理解,以下的一些變數都是中文命名的,實際生產不推薦)

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

image-20220827131530150

所以問題就變成了,如何將一個小數進行拆分呢?

二、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-20220827134012420

居然直接變成了 0

不過沒關係,需要可以配合一些 CSS 計算函式實現自動轉換,比如calc

count::before {
  --percent: 48.69;
  --整數: calc(48.69);/*使用 CSS 計算後可以轉換成整數*/
  --小數: 69;
  counter-reset: 整數 var(--整數) 小數 var(--小數);
  content: counter(整數) "." counter(小數);
}

image-20220827134338534

但是,這裡變成了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-20220827144136572

用程式碼實現就是

@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-20220827135400459

後面最末位的小數由於四捨五入的關係稍微有些偏差,沒關係,可以修正一下,加上0.01就行了。其次,還有一個問題,當小數位小於 10 的時候,計算出的結果可能是這樣

image-20220827140549249

那麼,這種情況就需要動態補零了。

關於“補零”的技巧,之前在這篇文章中有過詳細介紹: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-20220827141156639

然後,我們透過 JS 讓這個數字隨機變化

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

效果如下

Kapture 2022-08-27 at 14.14.47

但是,這樣太死板了,我們需要數字變化的時候有個動畫,可以直接透過 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

這兩種效果可以自行選擇,僅僅只是過渡的不同

試想一下,如果這個效果用 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(runjs.work)

RunJS,前端程式碼線上創作與分享。

四、總結和說明

以上就是全部內容了,一個還不錯的小技巧,你學會了嗎?

  1. CSS 變數不支援直接在content中渲染,但是可以藉助計數器初始化來實現
  2. CSS 計數器不支援小數初始化
  3. CSS 計數器支援小數的實現原理在於將小數拆分為整數、小數點、小數三個部分
  4. CSS 自定義變數可以指定變數的型別,這樣透過 CSS 數學函式可以將一個小數轉換成整數
  5. 小數部分可以透過減去整數部分得到
  6. 小數部分還需要透過decimal-leading-zero補全位數
  7. CSS 單一變數一方面可以帶來更好的可維護性,另一方面還可以更輕易地實現過渡動畫
  8. 藉助 @property可以很方便的控制 CSS 變數的過渡和動畫

數字變化動畫在一些資料大屏展示的場景下還是挺實用的,有了 CSS 變數,再也不需要透過 JS 去實時計算了。不過目前相容性還不是太好,適合內部專案小範圍使用(當然直接用了不要緊,不支援的只是沒有動畫而已)。最後,如果覺得還不錯,對你有幫助的話,歡迎點贊、收藏、轉發❤❤❤

歡迎關注我的公眾號:前端偵探