實現preserveAspectRatio 中 meetOrSlice算法

1、前言

本文重點是實現preserveAspectRatio<meetOrSlice>參數的效果,也是background-size中值爲covercontain屬性效果。在實現以前,簡單介紹一下SVG中的viewport, viewBoxpreserveAspectRatio的關係。javascript

畫布

當咱們建立SVG標籤時,其實是建立了一個隱形的無限延伸的畫布。css

視窗(viewport)

畫布是無限延伸的,可是人的視野是有限的,咱們須要設置一個固定的區域,而後在這個固定的區域去繪製圖形,使其可見。因此咱們經過給SVG設置widthheight屬性(或者經過css設置寬高)設置一個可見區域,這個區域就是viewport,也就是視窗。這個區域是初始座標系。html

在不作任何座標系轉換 (transform) 的狀況下,咱們繪製一個SVG圖形,實際是繪製在畫布上,可是是以viewport的座標系爲參考座標系繪製的。能夠理解爲畫布是繪製實體,視窗是用來肯定具體繪製的位置和尺寸的。html5

viewBox

什麼是viewBox呢?java

能夠理解爲咱們手裏有一個任意尺寸的方形框,即viewBox,這個框能夠在視窗的區域內任意位置遊走。咱們經過設置viewBox的屬性值,能夠指定這個框的尺寸(width, height),以及具體遊走在視窗的那個位置(x, y)。git

在框內的畫面就是咱們最終看到的畫面,咱們將框內的區域「裁剪」出來,而後經過縮放填充整個視窗。而具體的縮放規則就要看preserveAspectRatio屬性設置的值。github

preserveAspectRatio

咱們經過viewBox裁剪了一個區域,而後將這個區域縮放填充整個視窗。爲了便於理解,咱們將經過viewBox裁剪出來的區域稱爲content,將視窗稱爲box算法

爲了使content在縮放過程不變形,咱們須要保持content的寬高比,若是contentbox的寬高比相等,則content經過縮放能夠恰好填滿整個box。可是若是寬高比不相等,那應該如何填充?bash

preserveAspectRatio就是爲了解決這個問題而產生的。preserveAspectRatio中有兩個參數<align><meetOrSlice>,一個值決定content按照什麼規則縮放,一個值決定縮放後的contentbox的對齊方式。svg

其中<meetOrSlice>屬性值的效果和background-sizecovercontain 相似。

關於更詳細的SVG座標系的解釋,參考這兩篇文章:

理解SVG座標系統和變換: 創建新視窗

理解SVG座標系和變換:視窗,viewBox和preserveAspectRatio

接下來就詳細介紹一下<meetOrSlice>是如何控制填充效果的。

2、meetOrSlice定義

爲了行文方便,仍然用contentbox分別代指用於填充的矩形圖和被填充的盒子。由於本文重點在於探索和實現preserveAspectRatio<meetOrSlice>效果,在這裏就不對<align>參數展開介紹了。

<meetOrSlice>參數有兩個值:meetslice,其中meet相似於background-size中的containslice相似於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)完整的contentbox內部可能產生空白。重點:不產生「越界」現象

slice / cover:縮放,使content覆蓋(cover)box所有區域,content可能會超出box區域。重點:不產生空白區域

3、理解meetOrSlice的填充規則

在瞭解了這個兩個屬性值的含義以後,咱們再來探究這兩個值的具體計算規則。boxcontent都是矩形,咱們將矩形分爲三類:正方形、豎向矩形、橫向矩形:

boxcontent分別有多是其中的任意一類,經過排列組合能夠獲得9種對應關係(相等,同向,異向):

爲了更精確的對比全部尺寸獲得的結果,咱們分別設置三種類型的box尺寸,以及三種類型的content尺寸,以下:

SVG中,設置meetslice,獲得的結果以下(SVG縮放中會影響到矩形的邊框寬度,因此一樣的邊框寬度由於縮放比例不一樣會致使最終的視覺寬度不一樣):

根據定義和實驗結果,咱們能夠發現:

  • meet模式下,爲了使 content最大程度的被完整包含在box內部,老是content長邊box對應邊對齊。

  • slice模式下,爲了使box被填滿,老是content短邊box對應邊對齊。

但這兩點並不能包含所有狀況,咱們看👇例子:

從上面的4個例子中能夠發現,當boxcontent都是同向的矩形(均爲橫向或者豎向)時,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的高(對齊高)
    • 短邊異邊(異邊:同上描述):假設content爲橫向,短邊爲高,box爲豎向,短邊爲寬,(content更扁),使content的高等於box的高(對齊高),反之同理。

如今,咱們已經知道meetslice的縮放規律,根據圖,咱們能夠進一步概括邏輯。在這裏,根據矩形形狀的特色,使用「扁」做爲統一標準:同寬狀況下,高度越小,越扁。

在保持content寬高比縮放的狀況下,比較contentbox的「扁」度:

  • meet

    • box > content => 對齊高:content的寬高同時乘以一個值使content的高等於box
    • box < content => 對齊寬:content的寬高同時乘以一個值使content的寬等於box
  • slice

    • box > content => 對齊寬:同上
    • box < content =>對齊高: 同上

僞代碼能夠這樣描述:

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_寬;
  }
}
複製代碼

4、實現meetOrSlice方法

根據僞代碼的描述,爲了真正實現這個算法,咱們須要:

  • 獲得一個矩形的「」度

  • 獲得scale

其實上面兩點很是容易獲取:

  • 若是一個矩形越扁,意味着寬高比越大,因此能夠經過寬高比 width / height 來獲取「扁」度。

  • scale,在僞代碼中其實已經能發現scale的計算方法(假設對齊寬):

contentW * scale = boxW;
scale = boxW / contentW;


因此縮放後的content寬高爲:
newContentW = scale * contentW;
newContentH = scale * contentH;
複製代碼

由此,咱們就能夠寫出meetslice方法了:

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對比一下效果,其中關於邊框的兩個問題:

  • svg中邊框寬度不一致:是由於SVG縮放中會連同邊框一塊兒縮放
  • svg中部分 部分邊框被裁切:是由於svg內部矩形的x,y定位,是以邊框的中線爲標準計算的,因此當座標點爲(0,0)時,會有一半的邊框超出svg的窗口範圍,致使被截斷。

點擊查看源碼

完結,撒花🎉。

參考

理解SVG座標系統和變換: 創建新視窗

理解SVG座標系和變換:視窗,viewBox和preserveAspectRatio

MDN preserveAspectRatio

相關文章
相關標籤/搜索