字體構造與文字垂直居中方案探索

2020-9-6    seo達人

1. 引子

垂直居中基本上是入門 CSS 必須要掌握的問題了,我們肯定在各種教程中都看到過“CSS 垂直居中的 N 種方法”,通常來說,這些方法已經(jīng)可以滿足各種使用場景了,然而當我們碰到了需要使用某些特殊字體進行混排、或者使文字對齊圖標的情況時,也許會發(fā)現(xiàn),無論使用哪種垂直居中的方法,總是感覺文字向上或向下偏移了幾像素,不得不專門對它們進行位移,為什么會出現(xiàn)這種情況呢?

2. 常見的垂直居中的方法

下圖是一個使用各種常見的垂直居中的方法來居中文字的示例,其中涉及到不同字體的混排,可以看出,雖然這里面用了幾種常用的垂直居中的方法,但是在實際的觀感上這些文字都沒有恰好垂直居中,有些文字看起來比較居中,而有些文字則偏移得很厲害。
垂直居中示例圖
在線查看:CodePen(字體文件直接引用了谷歌字體,如果沒有效果需要注意網(wǎng)絡情況)

通過設置 vertical-align:middle 對文字進行垂直居中時,父元素需要設置 font-size: 0,因為 vertical-align:middle 是將子元素的中點與父元素的 baseline + x-height / 2 的位置進行對齊的,設置字號為 0 可以保證讓這些線的位置都重合在中點。
我們用鼠標選中這些文字,就能發(fā)現(xiàn)選中的區(qū)域確實是在父層容器里垂直居中的,那么為什么文字卻各有高低呢?這里就涉及到了字體本身的構造和相關的度量值。

3. 字體的構造和度量

這里先提出一個問題,我們在 CSS 中給文字設置了 font-size,這個值實際設置的是字體的什么屬性呢?
下面的圖給出了一個示例,文字所在的標簽均為 span,對每種字體的文字都設置了紅色的 outline 以便觀察,且設有 line-height: normal。從圖中可以看出,雖然這些文字的字號都是 40px,但是他們的寬高都各不相同,所以字號并非設置了文字實際顯示的大小。
文字大小示意圖
為了解答這個問題,我們需要對字體進行深入了解,以下這些內(nèi)容是西文字體的相關概念。首先一個字體會有一個 EM Square(也被稱為 UPM、em、em size)[4],這個值最初在排版中表示一個字體中大寫 M 的寬度,以這個值構成一個正方形,那么所有字母都可以被容納進去,此時這個值實際反映的就成了字體容器的高度。在金屬活字中,這個容器就是每個字符的金屬塊,在一種字體里,它們的高度都是統(tǒng)一的,這樣每個字模都可以放入印刷工具中并進行排印。在數(shù)碼排印中,em 是一個被設置了大小的方格,計量單位是一種相對單位,會根據(jù)實際字體大小縮放,例如 1000 單位的字體設置了 16pt 的字號,那么這里 1000 單位的大小就是 16pt。Em 在 OpenType 字體中通常為 1000 ,在 TrueType 字體中通常為 1024 或 2048(2 的 n 次冪)。
金屬活字

金屬活字,圖片來自 http://designwithfontforge.com/en-US/The_EM_Square.html

3.1 字體度量

字體本身還有很多概念和度量值(metrics),這里介紹幾個常見的概念,以維基百科的這張圖為例(下面的度量值的計量單位均為基于 em 的相對單位):
字體結構

  • baseline:Baseline(基線)是字母放置的水平線。
  • x height:X height(x字高)表示基線上小寫字母 x 的高度。
  • capital height:Capital height(大寫高度)表示基線上一個大寫字母的高度。
  • ascender / ascent:Ascender(升部)表示小寫字母超出 x字高的字干,為了辨識性,ascender 的高度可能會比 capital height 大一點。Ascent 則表示文字頂部到 baseline 的距離。

字符升部

  • descender / descent:Descender(降部)表示擴展到基線以下的小寫字母的字干,如 j、g 等字母的底部。Descent 表示文字底部到 baseline 的距離。
  • line gap:Line gap 表示 descent 底部到下一行 ascent 頂部的距離。這個詞我沒有找到合適的中文翻譯,需要注意的是這個值不是行距(leading),行距表示兩行文字的基線間的距離。

接下來我們在 FontForge 軟件里看看這些值的取值,這里以 Arial 字體給出一個例子:
Arial Font Information
從圖中可以看出,在 General 菜單中,Arial 的 em size 是 2048,字體的 ascent 是1638,descent 是410,在 OS/2 菜單的 Metrics 信息中,可以得到 capital height 是 1467,x height 為 1062,line gap 為 67。
然而這里需要注意,盡管我們在 General 菜單中得到了 ascent 和 descent 的取值,但是這個值應該僅用于字體的設計,它們的和永遠為 em size;而計算機在實際進行渲染的時候是按照 OS/2 菜單中對應的值來計算,一般操作系統(tǒng)會使用 hhea(Horizontal Header Table)表的 HHead Ascent 和 HHead Descent,而 Windows 是個特例,會使用 Win Ascent 和 Win Descent。通常來說,實際用于渲染的 ascent 和 descent 取值要比用于字體設計的大,這是因為多出來的區(qū)域通常會留給注音符號或用來控制行間距,如下圖所示,字母頂部的水平線即為第一張圖中 ascent 高度 1638,而注音符號均超過了這個區(qū)域。根據(jù)資料的說法[5],在一些軟件中,如果文字內(nèi)容超過用于渲染的 ascent 和 descent,就會被截斷,不過我在瀏覽器里實驗后發(fā)現(xiàn)瀏覽器并沒有做這個截斷(Edge 86.0.608.0 Canary (64 bit), MacOS 10.15.6)。
ascent
在本文中,我們將后面提到的 ascent 和 descent 均認為是 OS/2 選項中讀取到的用于渲染的 ascent 和 descent 值,同時我們將 ascent + descent 的值叫做 content-area。

理論上一個字體在 Windows 和 MacOS 上的渲染應該保持一致,即各自系統(tǒng)上的 ascent 和 descent 應該相同,然而有些字體在設計時不知道出于什么原因,導致其確實在兩個系統(tǒng)中有不同的表現(xiàn)。以下是 Roboto 的例子:
Differences between Win and HHead metrics cause the font to be rendered differently on Windows vs. iOS (or Mac I assume) · Issue #267 · googlefonts/roboto
那么回到本節(jié)一開始的問題,CSS 中的 font-size 設置的值表示什么,想必我們已經(jīng)有了答案,那就是一個字體 em size 對應的大??;而文字在設置了 line-height: normal 時,行高的取值則為 content-area + line-gap,即文本實際撐起來的高度。
知道了這些,我們就不難算出一個字體的顯示效果,上面 Arial 字體在 line-height: normal 和 font-size: 100px 時撐起的高度為 (1854 + 434 + 67) / 2048 * 100px = 115px。
在實驗中發(fā)現(xiàn),對于一個行內(nèi)元素,鼠標拉取的 selection 高度為當前行 line-height 最高的元素值。如果是塊狀元素,當 line-height 的值為大于 content-area 時,selection 高度為 line-height,當其小于等于 content-area 時,其高度為 content-area 的高度。

3.2 驗證 metrics 對文字渲染的影響

在中間插一個問題,我們應該都使用過 line-height 來給文字進行垂直居中,那么 line-height 實際是以字體的哪個部分的中點進行計算呢?為了驗證這個問題,我新建了一個很有“設計感”的字體,em size 設為 1000,ascent 為 800,descent 為 200,并對其分別設置了正常的和比較夸張的 metrics:
TestGap normal
TestGap exaggerate
上面圖中左邊是 FontForge 里設置的 metrics,右邊是實際顯示效果,文字字號設為 100px,四個字母均在父層的 flex 布局下垂直居中,四個字母的 line-height 分別為 0、1em、normal、3em,紅色邊框是元素的 outline,黃色背景是鼠標選取的背景。由上面兩張圖可以看出,字體的 metrics 對文字渲染位置的影響還是很大的。同時可以看出,在設置 line-height 時,雖然 line gap 參與了撐起取值為 normal 的空間,但是不參與文字垂直居中的計算,即垂直居中的中點始終是 content-area 的中點。
TestGap trimming
我們又對字體進行了微調(diào),使其 ascent 有一定偏移,這時可以看出 1em 行高的文字 outline 恰好在正中間,因此可以得出結論:在瀏覽器進行渲染時,em square 總是相對于 content-area 垂直居中。
說完了字體構造,又回到上一節(jié)的問題,為什么不同字體文字混排的時候進行垂直居中,文字各有高低呢?
在這個問題上,本文給出這樣一個結論,那就是因為不同字體的各項度量值均不相同,在進行垂直居中布局時,content-area 的中點與視覺的中點不統(tǒng)一,因此導致實際看起來存在位置偏移,下面這張圖是 Arial 字體的幾個中線位置:
Arial center line
從圖上可以看出來,大寫字母和小寫字母的視覺中線與整個字符的中線還是存在一定的偏移的。這里我沒有找到排版相關學科的定論,究竟以哪條線進行居中更符合人眼觀感的居中,以我個人的觀感來看,大寫字母的中線可能看起來更加舒服一點(尤其是與沒有小寫字母的內(nèi)容進行混排的時候)。

需要注意一點,這里選擇的 Arial 這個字體本身的偏移比較少,所以使用時整體感覺還是比較居中的,這并不代表其他字體也都是這樣。

3.3 中文字體

對于中文字體,本身的設計上沒有基線、升部、降部等說法,每個字都在一個方形盒子中。但是在計算機上顯示時,也在一定程度上沿用了西文字體的概念,通常來說,中文字體的方形盒子中文字體底端在 baseline 和 descender 之間,頂端超出一點 ascender,而標點符號正好在 baseline 上。

4. CSS 的解決方案

我們已經(jīng)了解了字體的相關概念,那么如何解決在使用字體時出現(xiàn)的偏移問題呢?
通過上面的內(nèi)容可以知道,文字顯示的偏移主要是視覺上的中點和渲染時的中點不一致導致的,那么我們只要把這個不一致修正過來,就可以實現(xiàn)視覺上的居中了。
為了實現(xiàn)這個目標,我們可以借助 vertical-align 這個屬性來完成。當 vertical-align 取值為數(shù)值的時候,該值就表示將子元素的基線與父元素基線的距離,其中正數(shù)朝上,負數(shù)朝下。
這里介紹的方案,是把某個字體下的文字通過計算設置 vertical-align 的數(shù)值偏移,使其大寫字母的視覺中點與用于計算垂直居中的點重合,這樣字體本身的屬性就不再影響居中的計算。
具體我們將通過以下的計算方法來獲?。菏紫任覀冃枰阎斍白煮w的 em-size,ascent,descent,capital height 這幾個值(如果不知道 em-size,也可以提供其他值與 em-size 的比值),以下依然以 Arial 為例:

const emSize = 2048; const ascent = 1854; const descent = 434; const capitalHeight = 1467

// 計算前需要已知給定的字體大小 const fontSize = FONT_SIZE; // 根據(jù)文字大小,求得文字的偏移 const verticalAlign = ((ascent - descent - capitalHeight) / emSize) * fontSize; return ( <span style={{ fontFamily: FONT_FAMILY, fontSize }}> <span style={{ verticalAlign }}>TEXT</span> </span> )

由此設置以后,外層 span 將表現(xiàn)得像一個普通的可替換元素參與行內(nèi)的布局,在一定程度上無視字體 metrics 的差異,可以使用各種方法對其進行垂直居中。
由于這種方案具有固定的計算步驟,因此可以根據(jù)具體的開發(fā)需求,將其封裝為組件、使用 CSS 自定義屬性或使用 CSS 預處理器對文本進行處理,通過傳入字體信息,就能修正文字垂直偏移。

5. 解決方案的局限性

雖然上述的方案可以在一定程度上解決文字垂直居中的問題,但是在實際使用中還存在著不方便的地方,我們需要在使用字體之前就知道字體的各項 metrics,在自定義字體較少的情況下,開發(fā)者可以手動使用 FontForge 等工具查看,然而當字體較多時,挨個查看還是比較麻煩的。
目前的一種思路是我們可以使用 Canvas 獲取字體的相關信息,如現(xiàn)在已經(jīng)有開源的獲取字體 metrics 的庫 FontMetrics.js。它的核心思想是使用 Canvas 渲染對應字體的文字,然后使用 getImageData 對渲染出來的內(nèi)容進行分析。如果在實際項目中,這種方案可能導致潛在的性能問題;而且這種方式獲取到的是渲染后的結果,部分字體作者在構建字體時并沒有嚴格將設計的 metrics 和字符對應,這也會導致獲取到的 metrics 不夠準確。
另一種思路是直接解析字體文件,拿到字體的 metrics 信息,如 opentype.js 這個項目。不過這種做法也不夠輕量,不適合在實際運行中使用,不過可以考慮在打包過程中自動執(zhí)行這個過程。
此外,目前的解決方案更多是偏向理論的方法,當文字本身字號較小的情況下,瀏覽器可能并不能按照預期的效果渲染,文字會根據(jù)所處的 DOM 環(huán)境不同而具有 1px 的偏移[9]。

6. 未來也許可行的解決方案 - CSS Houdini

CSS Houdini 提出了一個 Font Metrics 草案[6],可以針對文字渲染調(diào)整字體相關的 metrics。從目前的設計來看,可以調(diào)整 baseline 位置、字體的 em size,以及字體的邊界大小(即 content-area)等配置,通過這些可以解決因字體的屬性導致的排版問題。

[Exposed=Window] interface FontMetrics {
 readonly attribute double width;
 readonly attribute FrozenArray<double> advances;
 readonly attribute double boundingBoxLeft;
 readonly attribute double boundingBoxRight;
 readonly attribute double height;
 readonly attribute double emHeightAscent;
 readonly attribute double emHeightDescent;
 readonly attribute double boundingBoxAscent;
 readonly attribute double boundingBoxDescent;
 readonly attribute double fontBoundingBoxAscent;
 readonly attribute double fontBoundingBoxDescent;
 readonly attribute Baseline dominantBaseline;
 readonly attribute FrozenArray<Baseline> baselines;
 readonly attribute FrozenArray<Font> fonts;
};

css houdini
從 https://ishoudinireadyyet.com/ 這個網(wǎng)站上可以看到,目前 Font Metrics 依然在提議階段,還不能確定其 API 具體內(nèi)容,或者以后是否會存在這一個特性,因此只能說是一個在未來也許可行的文字排版處理方案。

7.總結

文本垂直居中的問題一直是 CSS 中最常見的問題,但是卻很難引起注意,我個人覺得是因為我們常用的微軟雅黑、蘋方等字體本身在設計上比較規(guī)范,在通常情況下都顯得比較居中。但是當一個字體不是那么“規(guī)范”時,傳統(tǒng)的各種方法似乎就有點無能為力了。
本文分析了導致了文字偏移的因素,并給出尋找文字垂直居中位置的方案。
由于涉及到 IFC 的問題本身就很復雜[7],關于內(nèi)聯(lián)元素使用 line-height 與 vertical-align 進行居中的各種小技巧因為與本文不是強相關,所以在文章內(nèi)也沒有提及,如果對這些內(nèi)容比較感興趣,也可以通過下面的參考資料尋找一些相關介紹。

藍藍設計www.bouu.cn )是一家專注而深入的界面設計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網(wǎng)站建設 、平面設計服務

分享本文至:

日歷

鏈接

個人資料

藍藍設計的小編 http://www.bouu.cn

存檔