实现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

相关文章
相关标签/搜索