WebGL Tutorial
and more

坐标系

撰写时间:2024-07-18

修订时间:2024-07-19

概述

SVG的坐标系有其较为独特的特点,涉及到视口、用户坐标系、视口坐标系、viewBox等术语,我们需明确这些术语的语义及其作用,才能用好SVG这个强大的绘图工具。

本章中,我们将一一剖析这些术语的由来,及其确切的含义。

最简单的坐标系

坐标系的目的在于确定元素的位置,以及大小、范围。

SVG的默认坐标系,原点在左上角,X正轴朝右,Y正轴朝下。我们称此坐标系为用户坐标系。

svg { border: 1px solid gray; }

通过widthheight属性值确定了svg的大小,由于未指定单位,默认为px

视口概念的引入

默认视口

下面在X轴为250px的位置开始绘制矩形。

svg { border: 1px solid gray; }

超出svg大小范围之外的部分未予以绘制。

理论上,坐标系的X轴与Y轴可无限延伸,没有终值。但在计算机中,屏幕是有物理尺寸大小限制的,我们只能在一定范围内绘制特定的图形。

因此,SVG引入了视口viewport)的概念,在坐标系的基础上,加入了宽、高值的因子,以确定SVG图像绘制的可见区域。

视口区域为一矩形。默认情况下,左上角位于用户坐标系的原点,其宽高值均取自svg元素的widthheight属性值。超出视口区域部分的图像不予绘制,我们称之为被裁剪掉了。

自定义视口

在最外层的svg中,嵌套使用另一个svg即可定义一新视口。

svg { border: 1px solid white; }

默认情况下,图像在整个svg的物理大小范围内进行绘制。也即,默认视口的大小即整个svg的大小。但上面,我们通过使用另一个嵌套的svg元素,在画布内居中定义了一个新的视口,则嵌套的svg所包含的所有子元素,将在此视口内进行绘制。

嵌套svg的各个属性值均使用了百分比的形式,这样做的好处是,当我们改变了最外层svg的宽高属性值,新视口总能居中定位。

嵌套svg的作用,不仅在画布的相应位置重新定义了一个新的视口,它还隐式地创建了一个新的坐标系。它所包含的所有子元素,上面仅为一个rect元素,即在此新的坐标系中进行绘制。因此,其所包含的任何元素所绘制出来的图像,均不会超出其所定义的视口范围。

除了svg元素之外,symbol元素也可以定义视口。

从多视口看视口坐标系

一个视口划定了一个图像绘制的区域,而一个svg元素可以含有多个视口。通过使用嵌套的svg可以建立起一个新的视口。

svg { border: 1px solid gray; }

上面,使用两个嵌套的svg,并在相关属性上使用百分比的形式来设置值,从而在最外层的svg的左上角区域及右下角区域,分别新建立了两个视口,用于绘制不同的图形。

可以看出,两个视口分别建立起了两个独立的坐标系,视口下的子元素使用其所属视口的坐标系。

这两个坐标系不同于唯一的用户坐标系,可以有各自不同的原点、坐标轴朝向及大小。我们称之为视口坐标系viewport coordinate)。

因此,SVG视口有两套坐标系,一套为用户坐标系,一套为视口坐标系。

SVG元素用户坐标系视口坐标系
原点大小原点大小
最外层svg(0, 0)200px × 200px(0, 0)200px × 200px
第一个子svg(0, 0)100px × 100px(0, 0)100px × 100px
第二个子svg(100, 100)100px × 100px(0, 0)100px × 100px

每个SVG子元素均使用父元素的视口坐标系。下面我们使用数值而非百分比的方式来重新体绘制两个视口。

svg { border: 1px solid gray; }

最外层的svg的视口坐标系,其原点位置位于(0, 0),大小为200px × 200px

作为两个子svg,通过下面的语句定义了两个新视口:

这两个子svgxy属性的值,均来自于它们的父元素svg的视口坐标系中的值。

而两个子svg下的元素,又各自使用了它们父级svg的视口坐标系:

因此,尽管它们的代码完全一样,却使用了两个不同的视口坐标系,从而在不同的地方绘制出同样的图像。

这里可以看出,在使用SVG绘制图形时,用户应使用视口坐标系来定位各个元素,而SVG在后台自动将视口坐标系转换为用户坐标系,从而在画布上绘制出图形。

一般情况下,我们较少用到多个视口。但SVG坐标系的概念是建立在视口的基础上的,即使只使用一个默认的视口,所使用的坐标系也是视口坐标系。因此,理解视口以及视口坐标系的概念很重要。

viewBox属性

问题的提出

现在来看一个具体用例。下面是第三方绘制的一个图形:

svg的大小为200px * 120px,然后,使用circlepath在上面绘制了一个圆内嵌等腰三角形。

现在,根据Boss的指令,我们要使用这个图形,且需将其放在100px × 200px的画布上。我们先设定好svg的大小,然后直接复制粘贴其子元素的代码,效果如下:

由于源svg的大小与目标svg的大小不一致,导致迁移后图形被裁剪掉了,且布局排列也不好看。

很明显,我们需要更改其源代码。但由于其使用了path元素,我们需要对其数据进行重新调整,工作量很大,且不一定完全吻合原来的图形。我们也可以通过平移、缩放等变换达到目的,但也得需要经过仔细调节。

无论采取哪种方式,其本质,就是要将原来的坐标系为200px × 120px的图形转换为现在的100px × 200px的坐标系中。并且,最好能保持原图形的宽高比率,如果可能,还能自动在水平垂直方向上设定排列方式。

viewBox的作用

viewBox属性应运而生。它可以将特定的用户坐标系映射至视口坐标系中。对于上面的代码,我们只需在svg元素中为其设置viewBox属性值就行了,其他代码无需更改。

svg { border: 1px solid gray; }

只需将viewBox的值设定为原来的坐标系0 0 200 120SVG就会自动将此坐标系按比率映射至0 0 100 200的新坐标系中。

源与目标的宽高比如下表:

宽高比轴向
200120200 ÷ 120 ≈ 1.67横屏
目标100200100 ÷ 200 = 0.5竖屏

源为横屏,目标为竖屏,上面的效果就是将一个横屏的图像按比例缩放至一个竖屏上,并在竖屏上居中排列。

可见,viewBox属性的主要目的,在于将原来已在特定坐标系中绘制的图形,按照一定的条件,映射至新的视口坐标系中。并且,由于源与目标的宽高比不一致,SVG会在相应的轴向上居中排列图形。

需注意的是,设定了viewBox属性值后,其各个子元素所使用的坐标系应为viewBox所指定的源坐标系,这个坐标系,我们称之为用户坐标系user coordinate system)。SVG会自动帮我们转换为目标坐标系,也即视口坐标系。

preserveAspectRatio

设置了viewBox后,我们还可以通过preserveAspectRatio属性进行进一步的精准控制。正如其属性名称,其作用为如何保持宽高比

none

横排:

svg { border: 1px solid gray; }

竖排:

svg { border: 1px solid gray; }

preserveAspectRatio的值为none时,图形也会缩放,但其缩放是不按原宽高比率来缩放,也即,将源坐标系的图形简单地映射至目标坐标系。其过程是,将源区域的4个角,分别拉至目标区域对应的4个角,或称,简单而粗暴的边角定位。而此时如果源与目标的宽高比率不一致,就会产生变形的效果。

如果我们需要按比率进行缩放,则由于目标区域与源区域可能不一致,就会产生两个问题。第一个问题:对于按比率缩放后的图形,如果目标区域无法完全容纳,是再进行进一步的缩放以显示全部,还是将超出区域外的部分裁剪掉?(meet or slice?)

第二个问题:如何在水平、垂直轴向上排列经重新缩放后或保留下来的图形?(alignment?)

因此,我们需要将preserveAspectRatio属性按特定的格式来设置其值,以回答上述这两个问题。

meet

meet表示按比率缩放,且保证在目标坐标系区域中绘制源坐标系中的全部图形。

横排

svg { border: 1px solid gray; }

由于目标坐标系比较宽,因此先将源图形的高与目标区域的高相匹配,再按源的宽高比求出实际宽度。此时在水平方向上会产生多余的空间,则可使用xMin, xMidxMax的值来分别进行水平方向上的左对齐、居中对齐或右对齐。

在上面代码中切换修改不同的参数值,查看运行效果。

尽管此时YMin, YMidYMax无论取何值都一样,但也必须同时提供。

竖排

svg { border: 1px solid gray; }

由于目标坐标系比较高,因此先将源图形的宽与目标区域的宽相匹配,再按源的宽高比求出实际高度。此时在垂直方向上会产生多余的空间,则可使用YMin, YMidYMax的值来分别进行垂直方向上的上对齐、居中对齐或下对齐。

在上面代码中切换修改不同的参数值,查看运行效果。

尽管此时xMin, xMidxMax无论取何值都一样,但也必须同时提供。

slice

slice表示按比率缩放,但超出目标坐标系区域外的图形将被裁剪。

svg { border: 1px solid gray; }

slice的缩放算法与meet的缩放算法不一样,其具体算法步骤如下:

第一步,先分别比较源与目标在宽、高上的缩放比率。

属性目标轴向缩放比率
width200300300 ÷ 200 = 1.5
height120240240 ÷ 120 = 2

第二步,取两个比率的最大值作为缩放因子,分别在源的宽、高上进行缩放。

属性缩放因子缩放后的值
width2002200 × 2 = 400
height1202120 × 2 = 240

第三步,将缩放后大小为400px × 240px的图像放在目标为300px × 240px的区域内绘制,超出部分将被裁剪掉。

上图中,红色边框区域为由viewBox所指定的源区域,从图中可以看出,因目标区域宽度不够,源图形的右边被裁剪掉了。

修改svgwidth值,变动值域为[300px, 400px],可以看到,随着宽度的增加,源图形被裁剪掉的右边渐渐显示出来。当其值为400px时,将可看到全部的红色边框。这是因为此时两个轴向上宽高比均为2,因此目标区域正好有空间完全容纳下经缩放后的图形。

将代码中的xMin分别改为xMidxMax,可清晰地看到红色边框移动的情况。因此,红色边框可帮我们快速认识到哪部分被保留下来。

任意修改svgwidthheight属性值,观察被保留下来的图像部分。例如,500px × 150px,等等。不管这两个值为多少,使用xMidYMid slice都会居中保留相应的部分。

symbol

基本图形

svg { border: 1px solid white; } rect, circle, path { stroke: black; } path { fill: none; } path.leg { fill: #F2B654; } rect.scr-border { fill: #42586C; } rect.screen { fill: #CDD5E0; } rect.leg { fill: #2F678A; } circle:nth-of-type(1) { fill: #8BDAF8; } circle:nth-of-type(2) { fill: #FFCE65; } circle:nth-of-type(3) { fill: #FF6B60; } circle:nth-of-type(4) { fill: #4FDBC7; }

我们在一个大小为300px × 300pxsvg上绘制出一张科技元素的图像。svg的外框已通过CSS用白色标出。

注意,这个svg只有widthheight属性,没有viewBox属性。并且,我们是在整个画布上绘制了图像。

转为symbol

参考资源

  1. The perserveAspectRatio attribute
  2. Painting: Filling, Stroking and Marker Symbols
  3. Basic Shapes
  4. Styling
  5. Appendix H: Property Index