SVG 坐标系
2021.07.25 “烟花”来临,风雨大作。枯坐家中也是无趣。遂梳理下SVG坐标系相关的内容,以打发时间。
本文中前三个例子比较简单,从例四开始代码见这里.
HTML 坐标系
坐标系为了定位和确定尺寸。
定位使用元组(坐标)(x, y)表示,有坐标就一定需要原点。
尺寸用元组(width, height)表示,有尺寸就一定需要单位,用来表示图形在真实世界(for 计算机图形系统——显示器)的大小。
在使用浏览器显示元素的时候,所有的坐标都是基于浏览器viewport的左上角定位的,浏览器的渲染引擎会根据页面元素的CSS样式规则,计算出某个html元素的位置、大小(布局),着色(绘制)。<svg></svg>作为一个html元素标签自然也不例外,所以svg的显示位置,首先决定于svg标签在html文档流中的位置。
以下图为例:
- 屏幕左上角为原点
- svg元素坐标为(100, 300),距离屏幕左边缘100,上边缘300,宽度为300,高度为150
上面说法中,缺失了一个很重要的元素就是单位,没有单位这些数值就没有意义。我们知道无论是svg还是html中的尺寸单位默认都是px,CSS支持其他的单位,最终这些单位都可以以某一规则转化成px。但CSS的px并非物理设备真实的像素,window.devicePixelRatio定义了不同设备真实像素和css px之间的转化关系。所以web页面中尺寸的单位转化逻辑大概如下图所示:
graph LR
otherUnits[css units other than px]
pxCss[css unints in px]
pxPhysical[px of physical device]
otherUnits -->|CSS rules| pxCss
pxCss -->|devicePixelRatio| pxPhysical
加上单位后前面图中SVG元素的位置和尺寸便可完全确定。
在下面的讨论中,如无特殊说明,px是指CSS像素而非物理像素。
SVG 坐标系
SVG元素是放在HTML文档流中的,所以该元素本身的定位和尺寸完全按照HTML标准来确定。**SVG坐标系是为了确定svg内部元素的定位和尺寸,并且坐标和尺寸的单位可以不固定。**后者是SVG能够Scalable的根本原因。
以下面svg为例
<svg width="200" height="150" viewBox="0 0 200 150">
<rect x="20" y="20" width="100" height="50" stroke="red" fill="white"/>
</svg>
该svg渲染出来如下图所示:
svg标签使用CSS添加了橙色边框,以提供视觉参考
通过rect标签的属性很容易知道矩形左上角定点的坐标为(20, 20),矩形的宽为100,高为50,不带单位。横轴的正方向是→,纵轴的正方向是↓。
那么问题来了这些数字的单位是多少?
要回答这个问题,我们先来回答另一个问题,svg元素的宽高是多少,带单位?
这个答案很简单,随便google一下就知道svg标签的width和height属性指定了svg元素的宽和高尺寸,单位是px。答案是200px,150px。
那么第一个问题的答案就是px了。
回答正确,不过并没有那么简单。我们看到svg标签还有一个viewBox属性,值为"0 0 200 150"。在上述例子中,如果未指定viewBox属性,默认也是这个值0 0 svgWidthInPx svgHeightInPx.在解释这个是什么意思之前,首先看两个改动的例子,应该就能从感性上知道这个四个数字分别代表什么意思了。
viewBox="-100 -75 200 150"
改变了viewBox的前两个值,分别降低了svg宽度和高度的一半。矩形的尺寸没有变,位置分别向右平移100px,向下平移了75px。
viewBox="0 0 100 75"
改变了viewBox的后两个值,降低为原来的一半,矩形的宽高似乎膨胀了一倍。有一部分已经超出了svg的可视范围,被裁减掉。
viewBox是干啥的
viewBox的存在就是为了在svg内部建立起坐标系,svg元素内部的所有元素的定位和尺寸都以该坐标系为基准。提到坐标系就必须有原点。要想把坐标系的位置和长度映射到真实世界,坐标系必须有单位。
- 原点: svg(橙色框)左上角的坐标以viewBox的前两个值确定,以此推出原点。
- 在例1中,svg左上角即为原点
(0, 0) - 例2中,svg左上角为
(-100, -75),由此可以推断(0, 0)点在svg元素的中心位置 - 如果viewBox宽高比跟svg宽高比不一致,原点的确定方式由
preserveAspectRatio共同决定
- 在例1中,svg左上角即为原点
- 单位:viewBox的后两个值,代表坐标系将svg的横向和纵向分了多少份,则每份的长度即可由svg的真实宽度和高度算出
- 例1中,真实宽度为200px,横向被分为200份,则每份为1px,所以在svg内部所有的数值横向一个单位就是1px。
- 例2中。真实宽度为200px,横向被分为100份,则每份为2px,所以矩形的宽为100*2px,相比于利益长度增加了一倍。
- 高度单位类似。viewBox的宽高比可以跟svg的宽高比不一致,纵向和横向的尺寸单位也可以不一样(见
preserveAspectRatio小节)。
总上所述,viewBox="x, y, w, h",在svg元素上建立了一个坐标系,svg的左上角坐标为(x, y),右下角坐标为(x+w, y+h)。坐标系中每个单位的长度单位确定依赖于svg的宽高属性和preserveAspectRatio属性。
preserveAspectRatio
在上面的例子中,之所以viewBox中建立的坐标系能够跟CSS尺寸(px)对应起来,除了svg元素的尺寸已经写死确定外,还因为svg的preserveAspectRatio有有个default值xMidYMid meet。
为什么我们会需要这个属性呢?
上面的例子中,viewBox的宽高比和和svg元素的宽高比是一样的都是4:3。所以我们很容易得出横向和纵向的尺寸单位。例1为1px,例2为2px,且横纵相等。可是如果两个宽高比不一样呢?
我们先看下面的例子。
<ViewBox
width='400'
height='300'
viewBox='0 0 100 100'
preserveAspectRatio='xMidYMid meet'
/>
显示如下图所示:
svg元素是一个宽高比为4:3的长方形,viewBox却是一个正方形。这个时候viewBox创建的坐标系原点在哪里,尺寸单位又是多少呢。
通过上图很明显我们可以看出来,纵向viewBox高度撑满svg元素,横向则在svg中居中,宽度跟高度相同(正方形嘛)。
回过头,我们来看下preserveAspectRatio这个属性的默认值xMidYMid meet。这个值的意思是
- viewBox的尺寸由相对于svg较长的边确定,确定方式是viewBox相对较长的边在对应方向上撑满svg。对于例4而言,viewBox相对长的边为纵向,所以高度撑满svg。因此确定坐标系中单位为(150/100 = 1.5px)。
meet部分决定了这个特性。 - 确定单位后,就可以算出来viewBox相比svg较短的边的实际长度。对于例四而言,viewBox相比svg较短的边为横向,即宽为(1.5px * 100 = 150px)。
- svg的宽度为200px,viewBox为150px,撑不满svg。
xMid决定了viewBox相对于svg居中对齐。对于viewBox相对短边是纵向(高度)时,yMind决定了viewBox在垂直方向上的对齐方式。 - 由此可以推断出坐标系原点相对于svg左上角定点向右偏移了25px + 0 * 1.5px, 向下偏移了0px + 0*1.5px。
由此我们可以抽象地描述下preserveAspectRatio属性:
该属性的值的格式为
graph LR
xMin[Min]
xMid[Mid]
xMax[Max]
yMin[Min]
yMid[Mid]
yMax[Max]
x --> xMin
x --> xMid
x --> xMax
xMin --> Y
xMid --> Y
xMax --> Y
Y --> yMin
Y --> yMid
Y --> yMax
yMin --> Space
yMid --> Space
yMax --> Space
Space --> meet
Space --> slice
-
如果viewbox宽高比和svg元素宽高比不同
meet,会将viewBox的相对长轴撑满svg的相对短轴。也就说确保整个viewBox都在svg的区域内。如例4slice,会将viewBox的相对短轴撑满svg,viewBox的相对长轴方向会超出viewBox的视图区域。如例五,
例五: xMidYMid slice -
对齐方式
Min左/顶对齐(例6,8)Mid水平/垂直居中对齐(例4,5)Max右/底对齐(例7,9)
svg元素的宽高
通过上面的叙述,我们可以知道viewBox创建的坐标系,其中的数字是不带单位的,实际尺寸由svg的宽高和preserveAspectRatio共同决定。为了叙述方便,后者先确定为xMidYMid meet。此时svg中元素的位置和尺寸仅跟svg元素的宽高相关,按比例缩放,因此svg是可以scalable的。例10中分别展示了三种svg宽高下的展示。
那假如svg的宽高并没有指定呢?
SVG规范里描述得很清楚,分两种情况:
- 如果viewBox也没给,默认等价于
<svg width="300" height="150" viewBox="0 0 300 150"></svg> - 如果viewBox给了,svg的宽度值为
auto。auto值对于块级元素而言是指自身的宽度(border-box)等于父级元素的宽度(content-box)。规范中并没有明确说明svg元素是块级和是内联元素。我们可以试一下。
<div>
<div style={{ width: '400px' }}>
<ViewBox viewBox='0 0 100 200' />
</div>
<ViewBox viewBox='0 0 100 200' />
</div>
结果如下图所示
- 对于第一个svg,其父元素宽度写死,则该svg也定死
- 对于第二个svg,其父元素随着viewport的变化而变化,svg宽度也随着变化
- 综上所述,svg值为auto时的表现跟block元素一致。
总结
- SVG是一种通过向量(坐标系)来绘制图形的工具
- SVG通过
viewBox和preserveAspectRatio属性来创建一个坐标系 - viewBox
- 前两个值用来确定viewBox的起始坐标
- 后两个值用来确定viewBox的宽和高
- preserveAspectRatio
meet/slice确定viewBox在如何适配SVG。如果是meet则适配SVG的短边,确保viewBox在svg的区域内,如果是slice则适配SVG的长边,viewBox的另一方向会有区域超出svg范围而被截掉。<x|Y><Min|Mid|Max>确定viewBox中没有跟SVG适配的那个方向如何对齐。- 在适配方式和对齐方式都确定后,SVG坐标的单位和原点位置即可确定。
- SVG坐标的单位和原点位置确定后,SVG内部所有元素的位置和尺寸即可确定。