本文重點是實現preserveAspectRatio
中<meetOrSlice>
參數的效果,也是background-size
中值爲cover
,contain
屬性效果。在實現以前,簡單介紹一下SVG中的viewport
, viewBox
和preserveAspectRatio
的關係。javascript
當咱們建立SVG標籤時,其實是建立了一個隱形的無限延伸的畫布。css
畫布是無限延伸的,可是人的視野是有限的,咱們須要設置一個固定的區域,而後在這個固定的區域去繪製圖形,使其可見。因此咱們經過給SVG設置width
和height
屬性(或者經過css設置寬高)設置一個可見區域,這個區域就是viewport,也就是視窗。這個區域是初始座標系。html
在不作任何座標系轉換 (transform) 的狀況下,咱們繪製一個SVG圖形,實際是繪製在畫布上,可是是以viewport的座標系爲參考座標系繪製的。能夠理解爲畫布是繪製實體,視窗是用來肯定具體繪製的位置和尺寸的。html5
什麼是viewBox呢?java
能夠理解爲咱們手裏有一個任意尺寸的方形框,即viewBox,這個框能夠在視窗的區域內任意位置遊走。咱們經過設置viewBox的屬性值,能夠指定這個框的尺寸(width, height),以及具體遊走在視窗的那個位置(x, y)。git
在框內的畫面就是咱們最終看到的畫面,咱們將框內的區域「裁剪」出來,而後經過縮放填充整個視窗。而具體的縮放規則就要看preserveAspectRatio屬性設置的值。github
咱們經過viewBox裁剪了一個區域,而後將這個區域縮放填充整個視窗。爲了便於理解,咱們將經過viewBox裁剪出來的區域稱爲content,將視窗稱爲box。算法
爲了使content在縮放過程不變形,咱們須要保持content的寬高比,若是content和box的寬高比相等,則content經過縮放能夠恰好填滿整個box。可是若是寬高比不相等,那應該如何填充?bash
preserveAspectRatio就是爲了解決這個問題而產生的。preserveAspectRatio中有兩個參數<align>
和<meetOrSlice>
,一個值決定content按照什麼規則縮放,一個值決定縮放後的content與box的對齊方式。svg
其中<meetOrSlice>
屬性值的效果和background-size
中cover
和contain
相似。
關於更詳細的SVG座標系的解釋,參考這兩篇文章:
理解SVG座標系和變換:視窗,viewBox和preserveAspectRatio
接下來就詳細介紹一下<meetOrSlice>
是如何控制填充效果的。
爲了行文方便,仍然用content和box分別代指用於填充的矩形圖和被填充的盒子。由於本文重點在於探索和實現preserveAspectRatio的<meetOrSlice>
效果,在這裏就不對<align>
參數展開介紹了。
<meetOrSlice>
參數有兩個值:meet
和slice
,其中meet
相似於background-size
中的contain
,slice
相似於background-size
中的cover
。
在MDN preserveAspectRatio對這兩個屬性值是這樣描述的:
- meet (默認值) 圖形將縮放到:
- 寬高比將會被保留
- 整個SVG的viewbox在視圖範圍內是可見的
- 儘量的放大SVG的viewbox,同時仍然知足其餘的條件。
在這種狀況下,若是圖形的寬高比和視圖窗口不匹配,則某些視圖將會超出viewbox範圍(即SVG的viewbox視圖將會比可視窗口小)。
- slice 圖形將縮放到:
- 寬高比將會被保留
- 整個視圖窗口將覆蓋viewbox
- SVG的viewbox屬性將會被儘量的縮小,可是仍然符合其餘標準。
在這種狀況下,若是SVG的viewbox寬高比與可視區域不匹配,則viewbox的某些區域將會延伸到視圖窗口外部(即SVG的viewbox將會比可視窗口大)。
簡單歸納就是兩句話,在保持content寬高比不變的狀況下:
meet / contain
:縮放,使box包含(contain)完整的content,box內部可能產生空白。重點:不產生「越界」現象
slice / cover
:縮放,使content覆蓋(cover)box所有區域,content可能會超出box區域。重點:不產生空白區域
在瞭解了這個兩個屬性值的含義以後,咱們再來探究這兩個值的具體計算規則。box和content都是矩形,咱們將矩形分爲三類:正方形、豎向矩形、橫向矩形:
box和 content分別有多是其中的任意一類,經過排列組合能夠獲得9種對應關係(相等,同向,異向):爲了更精確的對比全部尺寸獲得的結果,咱們分別設置三種類型的box尺寸,以及三種類型的content尺寸,以下:
在SVG中,設置meet
和slice
,獲得的結果以下(SVG縮放中會影響到矩形的邊框寬度,因此一樣的邊框寬度由於縮放比例不一樣會致使最終的視覺寬度不一樣):
根據定義和實驗結果,咱們能夠發現:
meet
模式下,爲了使 content最大程度的被完整包含在box內部,老是content的長邊與box對應邊對齊。
slice
模式下,爲了使box被填滿,老是content的短邊與box對應邊對齊。
但這兩點並不能包含所有狀況,咱們看👇例子:
從上面的4個例子中能夠發現,當box和content都是同向的矩形(均爲橫向或者豎向)時,meet
狀況下,還須要繼續判斷短邊的長度;而在slice
的狀況下還須要繼續判斷長邊的長度。
因此咱們能夠這樣描述:
meet
:老是比較二者的長邊:
長邊同邊(同邊:都是橫向或者豎向矩形):假設都爲橫向矩形,根據寬高比計算在同一個寬度下二者的高度,判斷哪一個的高度更高(反之同理):
contentHeight > boxHeight
(此時box更「扁」):爲了使content被完整包含在box裏,須要讓content的高等於box的高(對齊高)boxHeight > contentHeight
(此時content更「扁」):使content的寬等於box的寬(對齊寬)長邊異邊(異邊:一個爲橫向另外一個爲豎向):假設content爲橫向,長邊爲寬,box爲豎向,長邊爲高(能夠理解爲content更扁一點),則使content的寬等於box的寬(對齊寬),反之同理。
slice
:老是比較二者的短邊:
contentHeight > boxHeight
(此時box更「扁」) :使content的寬等於box的寬(對齊寬)boxHeight > contentHeight
(此時content更「扁」):使content的高等於box的高(對齊高)如今,咱們已經知道meet
和slice
的縮放規律,根據圖,咱們能夠進一步概括邏輯。在這裏,根據矩形形狀的特色,使用「扁」做爲統一標準:同寬狀況下,高度越小,越扁。
在保持content寬高比縮放的狀況下,比較content和box的「扁」度:
meet
slice
僞代碼能夠這樣描述:
if(type == 'meet'){
if(box_扁 > content_扁){
content_高 * scale = box_高;
content_寬 * scale = new_content_寬;
}else{
content_寬 * scale = box_寬;
content_高 * scale = new_content_高;
}
}else if(type == 'slice'){
if(box_扁 > content_扁){
content_寬 * scale = box_寬;
content_高 * scale = new_content_高;
}else{
content_高 * scale = box_高;
content_寬 * scale = new_content_寬;
}
}
複製代碼
根據僞代碼的描述,爲了真正實現這個算法,咱們須要:
獲得一個矩形的「扁」度
獲得scale值
其實上面兩點很是容易獲取:
若是一個矩形越扁,意味着寬高比越大,因此能夠經過寬高比 width / height
來獲取「扁」度。
而scale,在僞代碼中其實已經能發現scale的計算方法(假設對齊寬):
contentW * scale = boxW;
scale = boxW / contentW;
因此縮放後的content寬高爲:
newContentW = scale * contentW;
newContentH = scale * contentH;
複製代碼
由此,咱們就能夠寫出meet
和slice
方法了:
function meetOrSlice(type, boxW, boxH, contentW, contentH){
let boxRadio = boxW / boxH,
contentRadio = contentW / contentH,
scaleW = (boxW / contentW) || 1,
scaleH = (boxH / contentH) || 1,
scale = 1;
if(type == 'meet'){
scale = boxRadio >= contentRadio ? scaleH : scaleW;
}else if(type == 'slice'){
scale = boxRadio >= contentRadio ? scaleW : scaleH;
}
return {
w: scale * contentW,
h: scale * contentH
}
}
複製代碼
最後和SVG對比一下效果,其中關於邊框的兩個問題:
(0,0)
時,會有一半的邊框超出svg的窗口範圍,致使被截斷。完結,撒花🎉。