SVG

1,504 阅读15分钟

本文章旨在解析如何一步步实现数据大屏

什么是SVG?

  • SVG 指可伸缩矢量图形 (Scalable Vector Graphics)
  • SVG 用来定义用于网络的基于矢量的图形
  • SVG 使用 XML 格式定义图形
  • SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
  • SVG 是万维网联盟的标准
  • SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体

SVG 的历史和优势

在 2003 年一月,SVG 1.1 被确立为 W3C 标准。

参与定义 SVG 的组织有:太阳微系统、Adobe、苹果公司、IBM 以及柯达。

与其他图像格式相比,使用 SVG 的优势在于:

  • SVG 可被非常多的工具读取和修改(比如记事本)
  • SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性更强。
  • SVG 是可伸缩的
  • SVG 图像可在任何的分辨率下被高质量地打印
  • SVG 可在图像质量不下降的情况下被放大
  • SVG 图像中的文本是可选的,同时也是可搜索的(很适合制作地图)
  • SVG 可以与 Java 技术一起运行
  • SVG 是开放的标准
  • SVG 文件是纯粹的 XML

简单示例

<svg width="200" height="200" viewbox="0 0 100 100"></svg>
  • width: 宽度
  • height: 高度

viewBox

MDN上的说明是

viewBox属性允许指定一个给定的一组图形伸展以适应特定的容器元素。viewBox属性的值是一个包含4个参数的列表 min-x, min-y, width and height, 以空格或者逗号分隔开, 在用户空间中指定一个矩形区域映射到给定的元素,查看属性preserveAspectRatio。不允许宽度和高度为负值,0则禁用元素的呈现。

看到这个说明一开始并不是很懂

我们来看一下下面这张图

coordinate

前两个参数代表x,y,画布的左上角的坐标位置。
后两个参数代表width,height,画布大小。

嵌套坐标系统

可以将另一个svg元素插入到文档中来建立一个新的视口和坐标系统,也就是说svg中可以嵌套另一个svg,每个svg都有自己独立的视口和坐标系统。

基本形状

线段

line元素,使用x1,y1,x2,y2属性指定线段的起止点坐标。有如下特性:

特性说明
stroke-width笔画宽度,坐标网格线位于笔画的正中间,可以使用css的shape-rendering值来控制反锯齿特性
stroke笔画颜色
stroke-opacity线条的不透明度
stroke-dasharray虚线,由一系列数字组成,数字个数为偶数(负责会自动重复一遍使其为偶数),表示线长-间隙-线长-间隙...

Demo

矩形

rect元素,使用x,y,width,height表示一个矩形

特性说明
fill填充颜色
fill-opacity填充不透明度
stroke边框颜色
stroke-width边框宽度,边框是骑在矩形边界上的,一半在矩形外,一半在矩形内
rx/ry圆角矩形,最大值为矩形宽/高的一半,如果只指定了一个,则认为两个都为相同的值

Demo

圆和椭圆

circle元素表示圆,由cx,cy,r属性界定 ellipse元素表示椭圆,由cx,cy,rx,ry界定

特性说明
fill填充颜色
fill-opacity填充不透明度
stroke边框颜色
stroke-width边框宽度,边框是骑在圆的边界上的,一半在圆/椭圆外,一半在圆/椭圆内

Demo

路径

path命令

SVG中所有基本形状都是path的简写形式,但是建议使用简写形式,因为这样可以使SVG文档更可读。

path元素更通用,可以通过制定一系列相互连接的线、弧、曲线来绘制任意形状的轮廓,这些轮廓也可以填充或者绘制轮廓线,也可以用来定义裁剪区域或蒙版。

下表为path命令总结,其中大写表示绝对坐标,小写表示相对坐标:

命令参数说明
M mx y移动画笔到制定坐标
L lx y绘制一条到给定坐标的线
H hx绘制一条到给定x坐标的横线
V vy绘制一条到给定y坐标的垂线
A arx ry x-axis-rotation large-arc sweep x y圆弧曲线命令有7个参数,依次表示x方向半径、y方向半径、旋转角度、大圆标识、顺逆时针标识、目标点x、目标点y。大圆标识和顺逆时针以0和1表示。0表示小圆、逆时针
Q qx1 y1 x y绘制一条从当前点到x,y控制点为x1,y1的二次贝塞尔曲线
T tx y绘制一条从当前点到x,y的光滑二次贝塞尔曲线,控制点为前一个Q命令的控制点的中心对称点,如果没有前一条则已当前点为控制点。
C cx1 y1 x2 y2 x y绘制一条从当前点到x,y控制点为x1,y1 x2,y2的三次贝塞尔曲线
S sx2 y2 x y绘制一条从当前点到x,y的光滑三次贝塞尔曲线。第一个控制点为前一个C命令的第二个控制点的中心对称点,如果没有前一条曲线,则第一个控制点为当前的点。

路径的填充同样可以使用fill-rule属性指定填充规则,如果需要填充一个中空的形状,则只需要注意外侧路径顺逆时针方向和内侧空心区域顺逆时针方向即可。

贝塞尔曲线

贝塞尔曲线是一种使用数学方法描述的曲线,被广泛用于计算机图形学和动画中,在矢量图里,贝塞尔曲线用于定义可无限放大的光滑曲线。

公式

线性公式

给定点P0、P1,线性贝兹曲线只是一条两点之间的直线。这条线由下式给出:

线性贝兹曲线公式

且其等同于线性插值。

二次方公式

二次方贝兹曲线的路径由给定点P0、P1、P2的函数B(t)追踪:

二次方贝兹曲线公式

三次方公式

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝兹曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P3之前,走向P2方向的“长度有多长”。
曲线的参数形式为:

三次方贝兹曲线公式

现代的成象系统,如PostScript、Asymptote和Metafont,运用了以贝兹样条组成的三次贝兹曲线,用来描绘曲线轮廓。

一般参数公式

n阶贝兹曲线可如下推断。给定点P0、P1、…、Pn,其贝兹曲线即:

一般参数公式

如上公式可如下递归表达: 用表示由点P0、P1、…、Pn所决定的贝兹曲线。 用平常话来说,阶的贝兹曲线,即双阶贝兹曲线之间的插值。

公式说明

  1. 开始于P0并结束于Pn的曲线,即所谓的端点插值法属性。
  2. 曲线是直线的充分必要条件是所有的控制点都位在曲线上。同样的,贝塞尔曲线是直线的充分必要条件是控制点共线。
  3. 曲线的起始点(结束点)相切于贝塞尔多边形的第一节(最后一节)。
  4. 一条曲线可在任意点切割成两条或任意多条子曲线,每一条子曲线仍是贝塞尔曲线。
  5. 一些看似简单的曲线(如圆)无法以贝塞尔曲线精确的描述,或分段成贝塞尔曲线(虽然当每个内部控制点对单位圆上的外部控制点水平或垂直的的距离为时,分成四段的贝兹曲线,可以小于千分之一的最大半径误差近似于圆)。
  6. 位于固定偏移量的曲线(来自给定的贝塞尔曲线),又称作偏移曲线(假平行于原来的曲线,如两条铁轨之间的偏移)无法以贝兹曲线精确的形成(某些琐屑实例除外)。无论如何,现存的启发法通常可为实际用途中给出近似值。

what?

友谊的小船说翻就翻

我开始慌了

看到上面的公式一头雾水,怎么办?

别急,我们不管它,继续往下看......

贝塞尔曲线绘制

贝塞尔曲线的类型有很多,但是在path元素里,只存在两种贝塞尔曲线:三次贝塞尔曲线,和二次贝塞尔曲线。

我们从稍微复杂一点的三次贝塞尔曲线C入手,三次贝塞尔曲线需要定义一个点和两个控制点,所以用C命令创建三次贝塞尔曲线,需要设置三组坐标参数:

C x1 y1, x2 y2, x y (or c dx1 dy1, dx2 dy2, dx dy)

这里的最后一个坐标(x,y)表示的是曲线的终点,另外两个坐标是控制点,(x1,y1)是起点的控制点,(x2,y2)是终点的控制点。如果你熟悉代数或者微积分的话,会更容易理解控制点,控制点描述的是曲线起始点的斜率,曲线上各个点的斜率,是从起点斜率到终点斜率的渐变过程。

Cubic_Bezier_Curves

<svg width="190px" height="160px" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <path d="M10 10 C 20 20, 40 20, 50 10" stroke="black" fill="transparent"/>
  <path d="M70 10 C 70 20, 120 20, 120 10" stroke="black" fill="transparent"/>
  <path d="M130 10 C 120 20, 180 20, 170 10" stroke="black" fill="transparent"/>
  <path d="M10 60 C 20 80, 40 80, 50 60" stroke="black" fill="transparent"/>
  <path d="M70 60 C 70 80, 110 80, 110 60" stroke="black" fill="transparent"/>
  <path d="M130 60 C 120 80, 180 80, 170 60" stroke="black" fill="transparent"/>
  <path d="M10 110 C 20 140, 40 140, 50 110" stroke="black" fill="transparent"/>
  <path d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="transparent"/>
  <path d="M130 110 C 120 140, 180 140, 170 110" stroke="black" fill="transparent"/>
</svg>

上面的例子里,创建了9个三次贝塞尔曲线。

你可以将若干个贝塞尔曲线连起来,从而创建出一条很长的平滑曲线。通常情况下,一个点某一侧的控制点是它另一侧的控制点的对称(以保持斜率不变)。这样,你可以使用一个简写的贝塞尔曲线命令S,如下所示:

S x2 y2, x y (or s dx2 dy2, dx dy)

S命令可以用来创建与前面一样的贝塞尔曲线,但是,如果S命令跟在一个C或S命令后面,则它的第一个控制点会被假设成前一个命令曲线的第二个控制点的中心对称点。如果S命令单独使用,前面没有C或S命令,那当前点将作为第一个控制点。下面是S命令的语法示例,图中左侧红色标记的点对应的控制点即为蓝色标记点。

ShortCut_Cubic_Bezier

<svg width="190px" height="160px" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <path d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" stroke="black" fill="transparent"/>
</svg>

另一种可用的贝塞尔曲线是二次贝塞尔曲线Q,它比三次贝塞尔曲线简单,只需要一个控制点,用来确定起点和终点的曲线斜率。因此它需要两组参数,控制点和终点坐标。

Q x1 y1, x y (or q dx1 dy1, dx dy)

Quadratic_Bezier

<svg width="190px" height="160px" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <path d="M10 80 Q 95 10 180 80" stroke="black" fill="transparent"/>
</svg>

就像三次贝塞尔曲线有一个S命令,二次贝塞尔曲线有一个差不多的T命令,可以通过更简短的参数,延长二次贝塞尔曲线。

 T x y (or t dx dy)

和之前一样,快捷命令T会通过前一个控制点,推断出一个新的控制点。这意味着,在你的第一个控制点后面,可以只定义终点,就创建出一个相当复杂的曲线。需要注意的是,T命令前面必须是一个Q命令,或者是另一个T命令,才能达到这种效果。如果T单独使用,那么控制点就会被认为和终点是同一个点,所以画出来的将是一条直线。

Shortcut_Quadratic_Bezier

<svg width="190px" height="160px" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <path d="M10 80 Q 52.5 10, 95 80 T 180 80" stroke="black" fill="transparent"/>
</svg>

虽然三次贝塞尔曲线拥有更大的自由度,但是两种曲线能达到的效果总是差不多的。具体使用哪种曲线,通常取决于需求,以及对曲线对称性的依赖程度。

Demo

下面提供了两个网址,大家可以尝试绘制自己的贝塞尔曲线:

贝塞尔曲线编辑器
高阶贝塞尔曲线编辑器
贝塞尔曲线小游戏

渐变

填充图形或笔画除了使用fill,stroke纯色之外,还可以使用图案和渐变填充。

线性渐变

线性渐变是一系列颜色沿着一条直线过渡,在特定的位置指定想要的颜色,被称为渐变点。渐变点是渐变结构的一部分,颜色是表现的一部分。

线性渐变使用linearGradient元素表示:

<defs>
	<linearGradient id="linear">
		<stop offset="0%" style="stop-color:#ffcc00;"></stop>
		<stop offset="100%" style="stop-color:#0099cc;"></stop>
	</linearGradient>
</defs>
<rect x="20" y="20" width="200" height="100" style="fill:url(#linear);stroke:black;"></rect>

linear

stop元素有两个必要属性:offset和stop-color。offset属性用来指定在哪个点的颜色应该等于stop-color。offset的取值范围0%-100%。

stop元素的属性:

属性说明
offset必需,取值范围0%-100%
stop-color必需,对应offset位置点的颜色
stop-opacity对应offset位置点的不透明度

linearGradient元素属性:

属性说明
x1,y1渐变的起点位置,使用百分比表示,默认的渐变方向是从左到右
x2,y2渐变的终点位置,使用百分比表示
spreadMethod如果设置的offset不能覆盖整个对象,该怎么填充。pad:起点或终点颜色会扩展到对象边缘。repeat:渐变重复起点到终点的过程。reflect:渐变按终点-起点-终点的排列重复。

Demo

SVG 动画

CSS动画

现代浏览器都支持CSS梳理SVG动画,使用CSS处理SVG动画需要两个步骤:一是选中要运动的元素,然后设置将动画属性作为一个整体进行设置。二是告诉浏览器改变选中元素的哪个属性以及在动画的什么阶段。这些都定义在@keyframes说明符中。

下面我们来实现一个大小动态变化的圆:

        const x = 100;
        const y = 100;
        const radius = 20;
        const outerRadius = radius * 2.2;
        const color = '#5ac1dc';
        return (
            <svg width="200" height="200" viewBox="0 0 200 200">
                <defs>
                    <style>{`
                    .gs-service-dot-inner{animation:scaleDot 2s infinite;}
                    .gs-service-dot-outer{opacity:0.27;animation:scaleDot 2s infinite;}
                    @keyframes scaleDot {
                        0%{transform: scale(1);}
                        50%{transform: scale(1.3);}
                        100%{transform: scale(1);}
                    }`}</style>
                </defs>
                <circle
                    key="inner"
                    className="gs-service-dot-inner"
                    cx={x}
                    cy={y}
                    r={radius}
                    fill={color}
                    style={{ transformOrigin: `${x}px ${y}px` }}
                />
                <circle
                    key="outer"
                    className="gs-service-dot-outer"
                    cx={x}
                    cy={y}
                    r={outerRadius}
                    fill={color}
                    style={{ transformOrigin: `${x}px ${y}px` }}
                />
            </svg>
        );

Demo

如何画一条会动的曲线

我们需要用到两个属性

  • stroke-dasharray 可控制用来描边的点划线的图案范式。该属性是让你指定画出的线段每段的长度,第二个值是各段之间空隙的长度。
  • stroke-dashoffset 属性指定了dash模式到路径开始的距离。该属性是让你指定每个小段的起始偏移量。
<path stroke="#000" stroke-width="4.3" fill="none" d="…"

stroke-dasharray="988.00 988.00"

stroke-dashoffset="988.00"

/>

请将两个属性都设到最大值(线的总长度),然后缓慢的减少dashoffset属性值。哇塞,线被动态的画出来了!

线的长度你可以从DOM里获取:

var path = document.querySelector('.squiggle-container path');
path.getTotalLength();

之后你就可以使用CSS动画来减少dashoffset属性值,让线动态的画出来了。

Demo

自己尝试一下拖动修改这两个属性

沿着path运动

如果需要让过渡对象沿着复杂的路径运动,则需要使用animateMotion元素。

<circle cx="0" cy="0" r={2} fill="#FF4AA1">
	<animateMotion path="M20,20 C20,104 216,300 300,300" dur="2s" repeatCount="indefinite" rotate="auto"></animateMotion>
</circle>

上述示例中,可以使圆形沿着path="M20,20 C20,104 216,300 300,300"做轨迹运动(运动过程中,如果需要图形自动根据轨迹方向旋转,需要设置animateMotion元素的rotate属性值为auto)。

如果已经有了path轨迹,不想再在animateMotion元素中定义一次path,则可以使用mPath元素。mPath元素定义在animateMotion元素内部,通过href引用指定的路径即可。

<path id="gsPath" class="gs-edge-path" d="M20,20 C20,104 216,300 300,300"></path>
<circle cx="0" cy="0" r="2" fill="#FF4AA1">
	<animateMotion dur="2s" repeatCount="indefinite" rotate="auto">
		<mpath href="#gsPath"></mpath>
	</animateMotion>
</circle>

Demo

绘制饼图

计算出圆上某个点的坐标

公式 要绘制图形,我们首先要计算出圆上点的坐标。

我们以角度70度为例,绘制扇形区域,从圆心绘至顶点(顶点坐标为圆心坐标x圆心坐标y-半径r),再绘至70度角的另一个点,再回至圆心即绘成一个扇形。

扇形

因此,问题就到了求出另一点的坐标。 由上图公式我们得出,
x1的值为圆心坐标x + 半径r * cos(90 - 70)
y1的值为圆心坐标y - 半径r * sin(90 - 70)

我们可以使用JavaScript内置函数Math.sinMath.cos来计算。
但有一点要注意Math.sinMath.cos的参数是弧度,而非角度,因此计算时我们要做一次转换

弧度 = (2 * Math.PI / 360) * 角度

我们可以由下列公式计算出坐标:
x1的值为圆心坐标x + 半径r * cos((2 * Math.PI / 360) * (90 - 70))
y1的值为圆心坐标y - 半径r * sin((2 * Math.PI / 360) * (90 - 70))

<path d="M249.89,272.5l0-89.3a89.3,89.3,0,0,1,83.9,58.7Z"></path>

同理,我们也可以绘制出弧形

<path d="M249.89,183.2a89.3,89.3,0,0,1,83.9,58.7L373.4,227.5A131.43,131.43,0,0,0,249.89,141.07Z"></path>

弧形

文本

text元素以指定的x和y值作为元素内容第一个字符的基线位置,默认样式黑色填充、没有轮廓。

属性说明
font-family以空格分割的一系列字体名称或通用字体名称
font-size如果有多行文本,则font-size为平行的两条基线的距离
font-weight两个值:bold(粗体)和nromal(默认)
font-style常用的两个值:italic(斜体)和normal
text-decoration可能的值:none,underline(下划线),overline(上划线),line-through(删除线)
word-spacing单词之间的距离
letter-spacing字母之间的间距
text-anchor对齐方式:start,middle,end
textLength设置文本的长度
lengthAdjust在指定了textLength时,可以通过lengthAdjust属性设置字符的调整方式,值为 spacing(默认)时,只调整字符的间距。当值为spacingAndGlyphs时,同时调整字符间距和字符本身的大小

如果要使得文本沿着某条路径排列,则需要使用textPath元素。需要将文本放在textPath元素内部,然后使用textPath元素的href属性引用一个定义好的path元素。

<defs>
    <path id="ch_title_path_0" d="M249.89,162.135a110.365,110.365,0,0,1,103.7,72.6"></path>
</defs>
<text fill="#ffffff" font-size="20">
    <textPath href="#ch_title_path_0" startOffset="50%" text-anchor="middle" dominant-baseline="central">南亚区DC</textPath>
</text>

设置startOffset="50%"text-anchor="middle"使文本水平居中
设置dominant-baseline="central"使文本垂直居中 文本

rotate变换

根据指定的角度旋转坐标系统,默认的坐标系统中,角度的测量顺时针增加。

注意,除非另行指定,否则旋转以原点为中心。 此时可以通过平移+旋转的方式来指定旋转中心: translate(centerX,centerY) rotate(angle) translate(-centerX,-centerY)

但是有个更简单的方式:rotate(angle,centerX,centerY)

现在,我们已经绘制了饼图中的一块了

Piece

接下来,我们只要同样画出另外几块,旋转角度即可

rotate

再画上几块,即可拼成一个完整的饼图了

pie

Demo

参考文献:
MDN SVG 路径
SVG技术入门:如何画出一条会动的线
D3js SVG 基础