現代 CSS 解決方案:數學函數 Round

2023-09-04 12:00:55

在 CSS 中,存在許多數學函數,這些函數能夠通過簡單的計算操作來生成某些屬性值,例如在現代 CSS 解決方案:CSS 數學函數一文中,我們詳細介紹了

  • calc():用於計算任意長度、百分比或數值型資料,並將其作為 CSS 屬性值。
  • min() 和 max():用於比較一組數值中的最大值或最小值,也可以與任意長度、百分比或數值型資料一同使用。
  • clamp():用於將屬性值限制在一個範圍內,支援三個引數:最小值、推薦值和最大值。

現代 CSS 解決方案:CSS 原生支援的三角函數 一文中,給大家介紹了從 Chrome 111 開始也逐漸開始原生支援的三角函數:

  • sin()
  • cos()
  • tan()

而本文,我們將介紹另外一個非常有意思的數學函數 - round()。

何為 round()?

簡單來說,round() CSS 函數的作用就是根據選定的舍入策略返回舍入數

舉個例子,在 JavaScript 中,我們可以使用 Math.round() 返回一個數位四捨五入後最接近的整數。

譬如:

x = Math.round(20.49); //20
x = Math.round(20.5); //21
x = Math.round(-20.5); //-20
x = Math.round(-20.51); //-21

現在,CSS 藉助 round() 函數也有了相同的能力:

line-height: round(2.2, 1); /* 2 */
line-height: round(14.82, 1); /* 15 */
line-height: round(5.5, 1); /* 6 */

也就是說,round(2.2, 1) 中的 2.2 四捨五入後,最後的計算值是 2。

round() 完整語法

round() 的完整語法規則還是比較複雜的。完整的介紹可以看 MDN - round()

使用它,可以完美實現類似於 JavaScript 中的如下幾個方法:

  1. Math.ceil()
  2. Math.floor()
  3. Math.round()
  4. Math.trunc()

它的完整語法規則:

<round()> = round( <rounding-strategy>?, <valueToRound> , <roundingInterval> )

可以看到,它最多可以接收 3 個引數,並且第一個引數是可選引數:

  • <rounding-strategy>:可選引數,表示舍入策略。 這可能是以下值之一:
    • up: 相當於 JavaScript Math.ceil() 方法,將 valueToRound 向上舍入到 roundingInterval 最接近的整數倍。 這相當於 JavaScript Math.ceil() 方法。
    • down:將 valueToRound 向下舍入為 roundingInterval 最接近的整數倍。 這相當於 JavaScript Math.floor() 方法。
    • nearest:將 valueToRound 舍入為 roundingInterval 的最接近的整數倍,該倍數可以高於或低於該值。 如果 valueToRound 是上方和下方舍入目標之間的一半,則會向上舍入。 相當於 JavaScript Math.round()。
    • to-zero:將 valueToRound 舍入為 roundingInterval 接近/接近零的最接近整數倍。 這相當於 JavaScript Math.trunc() 方法。
  • <valueToRound>:需要被四捨五入的值。 必須是 <number><dimension><percentage>,或者解析為這些值之一的數學表示式。
  • <roundingInterval>:舍入的間隔規則。 這是一個 <number><dimension><percentage>,或者解析為這些值之一的數學表示式。

基於此,舉幾個例子:

<div class="box-1"></div>
<div class="box-2"></div>
<div class="box-3"></div>
<div class="box-4"></div>
<div class="box-5"></div>
:root {
  --rounding-interval: 25px;
}
div {
  width: 100px;
  background: rgba(255, 100, 0, .8);
}
div.box-1 {
  height: round(nearest, 110px, var(--rounding-interval)); /* 最終計算值:100px */
}
div.box-2 {
  height: round(up, 110px, var(--rounding-interval)); /* 最終計算值:125px */
}
div.box-3 {
  height: round(down, 120px, var(--rounding-interval)); /* 最終計算值:100px */
}
div.box-4 {
  height: round(to-zero, 120px, var(--rounding-interval)); /* 最終計算值:100px */
}
div.box-5 {
  height: round(120px, var(--rounding-interval)); /* 最終計算值:125px */
}

結果如下:

圖中背景一個格子的大小是 25px

完整的 DEMO 可以看這裡 CodePen Demo - CSS Math Function Round() Demo

round 能解決什麼問題?

OK,鋪墊了那麼久,我們下面進入實戰環節。

那麼,round() 函數在 CSS 中有什麼具體的作用嗎?能應用到什麼地方?

解決基於 transform 的模糊問題

在之前的 疑難雜症:運用 transform 導致文字模糊的現象探究 這篇文章中,我們介紹了一種基於transform 的模糊問題。

我們來回顧一下問題現象:

在我們的頁面中,經常會出現這樣的問題,一塊區域內的文字或者邊框,在展示的時候,變得特別的模糊,如下(資料經過脫敏處理):

正常而言,應該是這樣的:

emmm,可能大圖不是很明顯,我們取一細節對比,就非常直觀了:

那麼?什麼時候會觸發這種問題呢?在 Google 上,其實我們能搜到非常多類似的案例,總結而言:

  1. 當文字元素的某個祖先容器存在 transform: translate() 或者 transform: scale()transform 操作時,容易出現這種問題

當然,這只是必要條件,不是充分條件。繼續深入探究,會發現,必須還得同時滿足一些其它條件:

  1. 元素作用了 transform: translate() 或者 transform: scale() 後的計算值產生了非整數

譬如,上述案例觸發的 CSS 程式碼如下:

.container {
    position: absolute;
    width: 1104px; 
    height: 475px;
    top: 50%;
    transform: translateY(-50%);
    // ...
}

由於元素的高度為 475pxtranslateY(-50%) 等於 237.5px,非整數,才導致了內部的字型模糊。

但是,需要注意的是,並非所有產生的非整數都會導致了內部的字型模糊。

這裡有個簡單的示意:

還是上述的例子,當高度從 477px 一直調整到 469px 的過程中,只有 477px475px 導致了模糊,而 473, 471, 469 則沒有。所以,這也只是引發模糊的一個必要條件。

  1. 文字內容是否模糊還與螢幕有關,高清屏(dpr > 2)下不容易觸發,更多發生在普通螢幕下(dpr = 1)

在我實測的過程中還發現,這個現象基本只會發生在 dpr 為 1 的普通螢幕下。

類似於 MAC 的高清螢幕則不太會觸發這個問題。

dpr = 物理畫素 / 裝置獨立畫素,表示裝置畫素比。這個與我們通常說的視網膜屏(多倍屏,Retina屏)有關。裝置畫素比描述的是未縮放狀態下,物理畫素和裝置獨立畫素的初始比例關係。

  1. 並非所有瀏覽器都是這個表現,基本發生在 chromium 核心。

那麼,為何會發生這種現象?針對這個問題,沒有找到特別官方的回答,普遍的認為是因為:

由於瀏覽器將圖層拆分到 GPU 以進行 3D 轉換,而非整數的畫素偏移,使得 Chrome 在字型渲染的時候,不是那麼的精確

關於這個問題,感興趣的可以再看看這兩個討論:

使用 round() 函數解決模糊問題

在之前,上面的這個基於 transform 的問題基本是無解的,想要不模糊,就需要替換掉 transfrom 方法。

而在有了 round() 後,我們可以通過 round() 函數,保證作用了 transform: translate() 或者 transform: scale() 後的計算值一定是正整數,從而避免模糊問題。

譬如,原本的 CSS 如下:

.container {
    width: 50vw;
    height: 50vh;
    transform: translate(-50%, -50%);
}

此時,transform: translate() 的實際最終計算值是會出現小數的。因此,我們可以使用 round() 函數進行取整:

.container {
    width: 50vw;
    height: 50vh;
    transform: translate(round(-50%, 1px), round(-50%, 1px));
}

我們可以使用如下 JavaScript 程式碼,列印出 transform 實時的計算值。

window.addEventListener("resize", () => {
    const transform = getComputedStyle(document.querySelectorAll("div")[0]).transform;
    console.log("transform:", transform);
});

如果使用 transform: translate(-50%, -50%) resize 整個頁面,可以看到如下列印值:

可以看到,此時,transform: matrix(1, 0, 0, 1, -50.5, -106.75) 的中的後兩位,其實就是 transform: translate(-50.5px, 106.75px),是存在小數值的。

而使用了 transform: translate(round(-50%, 1px), round(-50%, 1px)) 後,將不會再出現小數值:

完整的程式碼,你可以戳這裡試一試:CodePen Demo -- round() Demo

藉由 round() 函數,我們成功的解決了一直以來,Chrome 中非常棘手的一個模糊問題!

使用 round() 模擬步驟緩動動畫

round() 還有一個有趣用法。我們可以使用 round() 實現類似於 CSS Animation 中的 steps() 步驟動畫的效果。

我們來看這麼一個 DEMO:

<div></div>
@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

div {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    background: conic-gradient(#fc0, #fc0 15deg, transparent 15deg, transparent 30deg);
    transform: rotate(var(--angle));
    animation: propertyRotate 2s infinite linear;
}

@keyframes propertyRotate {
    100% {
        --angle: 360deg;
    }
}

這裡,我們實現了這麼一個動畫效果:

我們可以利用 round(),把一個連貫動畫,拆解成步驟動畫:

div {
   // ...
   // transform: rotate(var(--angle));
   transform: rotate(round(var(--angle), 30deg));
}

上面,我們使用 transform: rotate(round(var(--angle), 30deg)) 替換了 transform: rotate(var(--angle))

round(var(--angle), 30deg) 保證了其取值只能是 30deg 的倍數或者 0deg。因此,我們可以得到和使用 stpes() 步驟動畫一樣的效果:

上面使用了 round() 的動畫,和如下的動畫效果是一致的:

div {
    transform: rotate(round(var(--angle), 30deg));
}
// 等同於
div {
    transform: rotate(var(--angle));
    animation: propertyRotate 2s infinite steps(12);
}

因此,使用 round(),我們也可以輕鬆的實現類似如下的 Loading 動畫效果:

完整的程式碼,你可以戳這裡進行了解:CodePen Demo -- CSS Math Function Round() Animation Demo

最後

好了,本文到此結束,希望本文對你有所幫助