本篇作为技术分享系列的第一篇,详细讲一下 SVG 的解析和绘制,这部分功能的研究和最终实现由团队的 @黄超超 同学负责,感谢提供技术文档和支持。
首先我们来看一下 SVG 的文件结构和组成
SVG (Scalable Vector Graphics) 是一种可缩放矢量图形,使用 XML 格式来定义,是一种 W3C 标准,图像在放大或改变尺寸的情况下其图形质量不会有所损失。
下面是一个简单的 SVG 的文件结构例子:
代码语言:javascript复制<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red"/>
</svg>
从文件结构来看,SVG 确实是一种标准的 XML 格式,而里面的元素,从字面上来看,是一个坐标为(100,50),半径为40,填充色为红色,线条为黑色,线宽为2的圆形。下面我们来看看 SVG 文件里面的基本元素和属性:
1. 结构元素
<defs>, <g>, <svg>, <symbol>, <use>
2. 图形元素
<line>, <circle>, <ellipse>, <polygon>, <polyline>, <rect>, <path>
这些标签相信大家都不陌生,几乎每种界面语言都有类似的标记。在 SVG 里,最常用的还是<path>, 用它可以表示前面所有的标签。
3. 特殊元素
<image> :图片,源通常由 base64 string 或 url 表示。它通常出现在这种场景:通过 PhotoShop 编辑一张图片后,导出为 SVG 格式,这时文件里就存在 <image> 标签,之后再导入到 AI 中进行路径编辑,导出为 SVG 格式,就有了一张可以描绘路径,又包含 <image> 底图的 SVG 文件了。
<text> :文本,设置文字内容和字体字号等信息后,就可以在 SVG 中显示这些文字。 <text> 支持 transform 属性,可以旋转缩放文字,同时还支持 style, css 代码可以直接添加进来。
完整的元素列表参考这里:https://developer.mozilla.org/zh-CN/docs/Web/SVG/element
4. 元素的若干属性
opacity, fill, stroke, stroke-width, stroke-miterlimit, fill-opacity, stroke-opacity, fill-rule, stroke-dasharray, stroke-dashoffset, stroke-linecap, stroke-linejoin, transform
这些都不难理解,代表了元素的透明度,填充,线条,变换等,因为 SVG 是 W3C 标准,所以以上这些外观属性,在 CSS 中都有对应的属性。另外,SVG 还支持其他的属性类型,如动画事件/动画定时/关键帧动画/图形属性/过滤器等,十分强大。
完整的属性列表参考这里:https://developer.mozilla.org/zh-CN/docs/Web/SVG/attribute
来看一个例子:自上而下,分别包含了 两个矩形,一个圆形,一个椭圆,一条直线,一条折线,一个多边形和一条自定义 path。
代码语言:javascript复制<?xml version="1.0" standalone="no"?>
<svg width="200" height="250" version="1.1" viewBox="0 0 200 250" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="30" height="30" stroke="black" fill="transparent" stroke-width="5"/>
<rect x="60" y="10" rx="10" ry="10" width="30" height="30" stroke="black" fill="transparent" stroke-width="5"/>
<circle cx="25" cy="75" r="20" stroke="red" fill="transparent" stroke-width="5"/>
<ellipse cx="75" cy="75" rx="20" ry="5" stroke="red" fill="transparent" stroke-width="5"/>
<line x1="10" x2="50" y1="110" y2="150" stroke="orange" fill="transparent" stroke-width="5"/>
<polyline points="60,110 65,120 70,115 75,130 80,125 85,140 90,135 95,150 100,145"
stroke="orange" fill="transparent" stroke-width="5"/>
<polygon points="50,160 55,180 70,180 60,190 65,205 50,195 35,205 40 190 30 180 45 180"
stroke="green" fill="transparent" stroke-width="5"/>
<path d="M20,230 Q40,205 50,230 T90,230" fill="none" stroke="blue" stroke-width="5"/>
</svg>
这里对上面的示例代码做一些补充说明:
① 计量单位 width height x y 等没有显示指定单位,这时我们认为单位就是 px。也可以明确指定单位 in cm 等,这时会根据当前设备的环境来换算为 px 显示。
② viewBox 定义了画布上可以显示的区域,格式为 “x y width height”,如上图的 viewBox=“0 0 200 250”,从(0,0)点开始,宽高 200 * 250的区域,SVG 的 width=“200”,height=“250”,所以当前缩放比是1. 如果 SVG width=“400” height=“500”,则会有两倍的放大效果。
③ path 和其他元素的对比 在 SVG 中 path 是最常用的元素,和 polyline 做对比,path 也可以通过 d 的设置完成一样的折线或曲线,而且只需要很少的点就可以创建平滑的曲线,但 polyline 需要设置大量的点才能达到平滑的效果。所以从制作难度和缩放效果看,path 是更好的选择。
接下来看一下 SVG 的绘制过程
首先说明绘制的两个基本原则:
1. 解析顺序和绘制顺序一致,都要遵守 XML 中元素的位置排列。借用上面的例子,SVG 中元素在 XML 中有固定的排列顺序,我们解析时会遵守这个顺序,绘制时同样也会遵守这个顺序。也就是说先出现的元素,会出现在绘制的底层,而后出现的元素,会出现在绘制的顶层,如果元素间位置有重叠,则会出现顶层元素遮挡底层元素的情况。
2. 子节点会继承父节点的一些属性,如 opacity,transform 等。这点在绘制时需要特别注意,opacity 等静态属性需要继承,而 transform 等属性需要做矩阵变换才能得到子节点最终 transform。
来画手绘视频中对 SVG 的处理过程
处理中遇到的一些特殊情况和处理
1、解析SVG文档时,忽略DTD验证
虽然是 DTD 是 XML 解析的标准验证方式,但是很多工具制作的 SVG,DTD 会缺失,所以解析时应该忽略 DTD 验证,不然会直接造成解析错误
2、解析SVG文档时,一些元素的属性值可能有多种分隔/表明方式
多边形的点集,元素的 transform,都是一个数字集合,集合的分割方式可能是 “空格”,“,” 也可能是其他符号,所以在解析时需要兼容多种分割方式。
颜色的表示,长度单位等,也可能会出现多种形式,如颜色有已知颜色和颜色值等形式,都需要做兼容。
3、元素的某些属性会继承父级元素
transform,透明度等属性,都需要考虑父级元素的继承关系。transform 会复杂一些,transform [3*2] 的 矩阵,会包括缩放/平移/旋转 等信息,子元素的平移信息,需要和父级元素做缩放相乘后,再做平移。
4、元素属性的默认值
很多工具导出的 SVG,是会忽略一些属性的,而这些属性如果没有值,我们是没办法正确显示的。所以我们需要针对它们设置默认值。例如 fill 默认应该是 none,stroke 默认是 black,stroke-width 默认为1px,fill-rule 默认为 nonzero。这里重点说一下 fill-rule,它分为 evenodd 和 nonzero 两种方式:
EvenOdd:确定一个点是否位于填充区域内的规则,具体方法是从该点沿任意方向画一条无限长的射线,然后计算该射线在给定形状中因交叉而形成的路径段数。 如果该数为奇数,则点在内部;如果为偶数,则点在外部。
Nonzero:确定一个点是否位于路径填充区域内的规则,具体方法是从该点沿任意方向画一条无限长的射线,然后检查形状段与该射线的交点。 从零开始计数,每当线段从左向右穿过该射线时加1,而每当路径段从右向左穿过该射线时减 1。 计算交点的数目后,如果结果为零,则说明该点位于路径外部。 否则,它位于路径内部。
5、解析顺序与渲染顺序,描边与填色的顺序
解析顺序和渲染顺序必须一致,并且和 XML 中的顺序一致,否则会出现错误的遮挡现象和绘制顺序倒转。描边和填色的顺序,基本原则是,单个元素的描边完成后,操作填色,然后再操作下一个元素。当然这里的填色可以灵活控制,比如保存所有填色,等所有描边完成后,一次性填色。
6、包含<image>标签的绘制
包含 <image> 标签的 SVG,处理起来会有些特殊的地方。这种 SVG 的存在,一般是画师通过 PS 编辑图片后,再导入 AI 中生成的 SVG。处理这种 SVG 的绘制时,基本思路是:解析 <image> 标签,当做 SVG 的底图,用一个透明遮罩挡住;然后解析后面的 <path> 标签,这是只需要解析 path 和 stroke,不需要 fill,用这里的 path 去涂抹底图,涂抹过的地方,透明遮罩失效,底图露出,就达到了涂抹出底图线条的目的。按照这个思路把底图涂抹出来,类似刮刮卡的感觉。
到这里,SVG 的基本知识、解析和绘制原理就介绍完了,当然这只是很基础的过程,在后面我们会整理出一些很特殊的 SVG 格式的解析和绘制思路,届时和大家分享,谢谢。