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

題圖

圖片來源: https://unsplash.com/photos/G...
本文做者:馮昊

1. 引子

垂直居中基本上是入門 CSS 必需要掌握的問題了,咱們確定在各類教程中都看到過「CSS 垂直居中的 N 種方法」,一般來講,這些方法已經能夠知足各類使用場景了,然而當咱們碰到了須要使用某些特殊字體進行混排、或者使文字對齊圖標的狀況時,也許會發現,不管使用哪一種垂直居中的方法,老是感受文字向上或向下偏移了幾像素,不得不專門對它們進行位移,爲何會出現這種狀況呢?css

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

下圖是一個使用各類常見的垂直居中的方法來居中文字的示例,其中涉及到不一樣字體的混排,能夠看出,雖然這裏面用了幾種經常使用的垂直居中的方法,可是在實際的觀感上這些文字都沒有剛好垂直居中,有些文字看起來比較居中,而有些文字則偏移得很厲害。
垂直居中示例圖
在線查看:CodePen(字體文件直接引用了谷歌字體,若是沒有效果須要注意網絡狀況)html

經過設置 vertical-align:middle 對文字進行垂直居中時,父元素須要設置 font-size: 0,由於 vertical-align:middle 是將子元素的中點與父元素的 baseline + x-height / 2 的位置進行對齊的,設置字號爲 0 能夠保證讓這些線的位置都重合在中點。
咱們用鼠標選中這些文字,就能發現選中的區域確實是在父層容器裏垂直居中的,那麼爲何文字卻各有高低呢?這裏就涉及到了字體自己的構造和相關的度量值。

3. 字體的構造和度量

這裏先提出一個問題,咱們在 CSS 中給文字設置了 font-size,這個值實際設置的是字體的什麼屬性呢?
下面的圖給出了一個示例,文字所在的標籤均爲 span,對每種字體的文字都設置了紅色的 outline 以便觀察,且設有 line-height: normal。從圖中能夠看出,雖然這些文字的字號都是 40px,可是他們的寬高都各不相同,因此字號並不是設置了文字實際顯示的大小。
文字大小示意圖
爲了解答這個問題,咱們須要對字體進行深刻了解,如下這些內容是西文字體的相關概念。首先一個字體會有一個 EM Square(也被稱爲 UPM、em、em size)[4],這個值最初在排版中表示一個字體中大寫 M 的寬度,以這個值構成一個正方形,那麼全部字母均可以被容納進去,此時這個值實際反映的就成了字體容器的高度。在金屬活字中,這個容器就是每一個字符的金屬塊,在一種字體裏,它們的高度都是統一的,這樣每一個字模均可以放入印刷工具中並進行排印。在數碼排印中,em 是一個被設置了大小的方格,計量單位是一種相對單位,會根據實際字體大小縮放,例如 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 的相對單位):
字體結構git

  • 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 菜單中對應的值來計算,通常操做系統會使用 hhea(Horizontal Header Table)表的 HHead Ascent 和 HHead Descent,而 Windows 是個特例,會使用 Win Ascent 和 Win Descent。一般來講,實際用於渲染的 ascent 和 descent 取值要比用於字體設計的大,這是由於多出來的區域一般會留給注音符號或用來控制行間距,以下圖所示,字母頂部的水平線即爲第一張圖中 ascent 高度 1638,而注音符號均超過了這個區域。根據資料的說法[5],在一些軟件中,若是文字內容超過用於渲染的 ascent 和 descent,就會被截斷,不過我在瀏覽器裏實驗後發現瀏覽器並無作這個截斷(Edge 86.0.608.0 Canary (64 bit), MacOS 10.15.6)。
ascent
在本文中,咱們將後面提到的 ascent 和 descent 均認爲是 OS/2 選項中讀取到的用於渲染的 ascent 和 descent 值,同時咱們將 ascent + descent 的值叫作 content-area。github

理論上一個字體在 Windows 和 MacOS 上的渲染應該保持一致,即各自系統上的 ascent 和 descent 應該相同,然而有些字體在設計時不知道出於什麼緣由,致使其確實在兩個系統中有不一樣的表現。如下是 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
那麼回到本節一開始的問題,CSS 中的 font-size 設置的值表示什麼,想必咱們已經有了答案,那就是一個字體 em size 對應的大小;而文字在設置了 line-height: normal 時,行高的取值則爲 content-area + line-gap,即文本實際撐起來的高度。
知道了這些,咱們就不難算出一個字體的顯示效果,上面 Arial 字體在 line-height: normalfont-size: 100px 時撐起的高度爲 (1854 + 434 + 67) / 2048 * 100px = 115px
在實驗中發現,對於一個行內元素,鼠標拉取的 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
咱們又對字體進行了微調,使其 ascent 有必定偏移,這時能夠看出 1em 行高的文字 outline 剛好在正中間,所以能夠得出結論:在瀏覽器進行渲染時,em square 老是相對於 content-area 垂直居中。
說完了字體構造,又回到上一節的問題,爲何不一樣字體文字混排的時候進行垂直居中,文字各有高低呢?
在這個問題上,本文給出這樣一個結論,那就是由於不一樣字體的各項度量值均不相同,在進行垂直居中佈局時,content-area 的中點與視覺的中點不統一,所以致使實際看起來存在位置偏移,下面這張圖是 Arial 字體的幾個中線位置:
Arial center line
從圖上能夠看出來,大寫字母和小寫字母的視覺中線與整個字符的中線仍是存在必定的偏移的。這裏我沒有找到排版相關學科的定論,究竟以哪條線進行居中更符合人眼觀感的居中,以我我的的觀感來看,大寫字母的中線可能看起來更加舒服一點(尤爲是與沒有小寫字母的內容進行混排的時候)。web

須要注意一點,這裏選擇的 Arial 這個字體自己的偏移比較少,因此使用時總體感受仍是比較居中的,這並不表明其餘字體也都是這樣。

3.3 中文字體

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

4. CSS 的解決方案

咱們已經瞭解了字體的相關概念,那麼如何解決在使用字體時出現的偏移問題呢?
經過上面的內容能夠知道,文字顯示的偏移主要是視覺上的中點和渲染時的中點不一致致使的,那麼咱們只要把這個不一致修正過來,就能夠實現視覺上的居中了。
爲了實現這個目標,咱們能夠藉助 vertical-align 這個屬性來完成。當 vertical-align 取值爲數值的時候,該值就表示將子元素的基線與父元素基線的距離,其中正數朝上,負數朝下。
這裏介紹的方案,是把某個字體下的文字經過計算設置 vertical-align 的數值偏移,使其大寫字母的視覺中點與用於計算垂直居中的點重合,這樣字體自己的屬性就再也不影響居中的計算。
具體咱們將經過如下的計算方法來獲取:首先咱們須要已知當前字體的 em-size,ascent,descent,capital height 這幾個值(若是不知道 em-size,也能夠提供其餘值與 em-size 的比值),如下依然以 Arial 爲例:api

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

由此設置之後,外層 span 將表現得像一個普通的可替換元素參與行內的佈局,在必定程度上無視字體 metrics 的差別,可使用各類方法對其進行垂直居中。
因爲這種方案具備固定的計算步驟,所以能夠根據具體的開發需求,將其封裝爲組件、使用 CSS 自定義屬性或使用 CSS 預處理器對文本進行處理,經過傳入字體信息,就能修正文字垂直偏移。瀏覽器

5. 解決方案的侷限性

雖然上述的方案能夠在必定程度上解決文字垂直居中的問題,可是在實際使用中還存在着不方便的地方,咱們須要在使用字體以前就知道字體的各項 metrics,在自定義字體較少的狀況下,開發者能夠手動使用 FontForge 等工具查看,然而當字體較多時,挨個查看仍是比較麻煩的。
目前的一種思路是咱們可使用 Canvas 獲取字體的相關信息,如如今已經有開源的獲取字體 metrics 的庫 FontMetrics.js。它的核心思想是使用 Canvas 渲染對應字體的文字,而後使用 getImageData 對渲染出來的內容進行分析。若是在實際項目中,這種方案可能致使潛在的性能問題;並且這種方式獲取到的是渲染後的結果,部分字體做者在構建字體時並無嚴格將設計的 metrics 和字符對應,這也會致使獲取到的 metrics 不夠準確。
另外一種思路是直接解析字體文件,拿到字體的 metrics 信息,如 opentype.js 這個項目。不過這種作法也不夠輕量,不適合在實際運行中使用,不過能夠考慮在打包過程當中自動執行這個過程。
此外,目前的解決方案更可能是偏向理論的方法,當文字自己字號較小的狀況下,瀏覽器可能並不能按照預期的效果渲染,文字會根據所處的 DOM 環境不一樣而具備 1px 的偏移[9]。網絡

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

CSS Houdini 提出了一個 Font Metrics 草案[6],能夠針對文字渲染調整字體相關的 metrics。從目前的設計來看,能夠調整 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/ 這個網站上能夠看到,目前 Font Metrics 依然在提議階段,還不能肯定其 API 具體內容,或者之後是否會存在這一個特性,所以只能說是一個在將來也許可行的文字排版處理方案。

7.總結

文本垂直居中的問題一直是 CSS 中最多見的問題,可是卻很難引發注意,我我的以爲是由於咱們經常使用的微軟雅黑、蘋方等字體自己在設計上比較規範,在一般狀況下都顯得比較居中。可是當一個字體不是那麼「規範」時,傳統的各類方法彷佛就有點無能爲力了。
本文分析了致使了文字偏移的因素,並給出尋找文字垂直居中位置的方案。
因爲涉及到 IFC 的問題自己就很複雜[7],關於內聯元素使用 line-heightvertical-align 進行居中的各類小技巧由於與本文不是強相關,因此在文章內也沒有說起,若是對這些內容比較感興趣,也能夠經過下面的參考資料尋找一些相關介紹。

相關資料

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!
相關文章
相關標籤/搜索