字體構(gòu)造與文字垂直居中方案探索

2020-9-6    seo達(dá)人

1. 引子

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

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

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

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

3. 字體的構(gòu)造和度量

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

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

3.1 字體度量

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

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

字符升部

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

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

理論上一個(gè)字體在 Windows 和 MacOS 上的渲染應(yīng)該保持一致,即各自系統(tǒng)上的 ascent 和 descent 應(yīng)該相同,然而有些字體在設(shè)計(jì)時(shí)不知道出于什么原因,導(dǎo)致其確實(shí)在兩個(gè)系統(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 設(shè)置的值表示什么,想必我們已經(jīng)有了答案,那就是一個(gè)字體 em size 對(duì)應(yīng)的大?。欢淖衷谠O(shè)置了 line-height: normal 時(shí),行高的取值則為 content-area + line-gap,即文本實(shí)際撐起來的高度。
知道了這些,我們就不難算出一個(gè)字體的顯示效果,上面 Arial 字體在 line-height: normal 和 font-size: 100px 時(shí)撐起的高度為 (1854 + 434 + 67) / 2048 * 100px = 115px。
在實(shí)驗(yàn)中發(fā)現(xiàn),對(duì)于一個(gè)行內(nèi)元素,鼠標(biāo)拉取的 selection 高度為當(dāng)前行 line-height 最高的元素值。如果是塊狀元素,當(dāng) line-height 的值為大于 content-area 時(shí),selection 高度為 line-height,當(dāng)其小于等于 content-area 時(shí),其高度為 content-area 的高度。

3.2 驗(yàn)證 metrics 對(duì)文字渲染的影響

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

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

3.3 中文字體

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

4. CSS 的解決方案

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

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

// 計(jì)算前需要已知給定的字體大小 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> )

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

5. 解決方案的局限性

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

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

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

[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/ 這個(gè)網(wǎng)站上可以看到,目前 Font Metrics 依然在提議階段,還不能確定其 API 具體內(nèi)容,或者以后是否會(huì)存在這一個(gè)特性,因此只能說是一個(gè)在未來也許可行的文字排版處理方案。

7.總結(jié)

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

藍(lán)藍(lán)設(shè)計(jì)m.sillybuy.com )是一家專注而深入的界面設(shè)計(jì)公司,為期望卓越的國(guó)內(nèi)外企業(yè)提供卓越的UI界面設(shè)計(jì)、BS界面設(shè)計(jì) 、 cs界面設(shè)計(jì) 、 ipad界面設(shè)計(jì) 、 包裝設(shè)計(jì) 、 圖標(biāo)定制 、 用戶體驗(yàn) 、交互設(shè)計(jì)、 網(wǎng)站建設(shè) 、平面設(shè)計(jì)服務(wù)

日歷

鏈接

個(gè)人資料

存檔