Motion Fun
首发于Motion Fun
「看相」黑科技:SVG 动画在 H5 项目中的运用

「看相」黑科技:SVG 动画在 H5 项目中的运用

本文原载 网易UEDC 微信公众号,原文链接:「看相」黑科技:SVG 动画在 H5 项目中的运用,作者……还是本人XD。

前言

前段时间有个神奇的 H5 刷爆了大家的朋友圈——来自网易云音乐的《刷脸生成你的12位图》。它能通过识别你拍摄或上传的照片帮你「看相」,分析你的各种属性值,最后为你推荐一首个性歌曲。当然,这里的「算命结果」仅供娱乐,但面部特征识别却是真正的「黑科技」。

我参与了这个项目的动效设计,甚至还动手写了一部分代码,和当时正在高考的孩子们一起惨遭数学的蹂躏……通过这篇文章我想和大家分享一些这个 H5 中的动效设计与落地经验,嗯,主要是用到了 SVG 这个神奇的图片格式。

还没玩过的朋友赶紧扫码体验一下 ☟☟☟




什么是 SVG

以下摘自维基百科:

SVG 是 Scalable Vector Graphics(可缩放矢量图形) 的缩写,相对于JPEG、PNG和GIF等格式,它具有如下特点:

  • 可以包含矢量和位图,矢量部分不会因为适配缩放而损失画质。
  • 图像文件可读(可以用文本编辑器编辑),易于修改和编辑。
  • 支持动画,与现有技术可以互动融合。例如,SVG 技术本身的动态部分(包括时序控制和动画)就是基于 SMIL 标准。另外,SVG 文件还可嵌入 JavaScript(严格地说,应该是ECMAScript)脚本来控制SVG对象。
  • SVG 图形格式可以方便的创建文字索引,从而实现基于内容的图像搜索。
  • SVG 图形格式支持多种滤镜和特殊效果,在不改变图像内容的前提下可以实现位图格式中类似文字阴影的效果。
  • SVG 图形格式可以用来动态生成图形。例如,可用 SVG 动态生成具有交互功能的地图,嵌入网页中,并显示给终端用户。

本文将主要介绍 SVG SMIL 动画。SMIL 全称是 Synchronized Multimedia Integration Language,即「同步多媒体集成语言」,简单来说能利用 SVG 内部元素的设置来驱动动画。另外常用的 SVG 动画手段还有 CSS 结合 SVG、JavaScript 结合 SVG 做动画,限于篇幅本文不做涉及。

我在之前的项目《网易云音乐陪你温暖同行》(网易云音乐 2017 年终总结)中就用到了 SVG 动画:








点击查看 SVG 版本:SVG 演示页面


这三组动画都是周期很长的循环动画,其中星球和落叶的动画占页面的面积都还不小,需要保证一定的清晰度。如果按照传统做法做成 GIF 或者 PNG 序列帧要么体积大,要么会很卡顿(实际上文章中展示的 GIF 图就是为了减小体积压缩得卡顿了,SVG 版本是丝般顺滑的),而如果我们使用 SVG 来实现,就只需要很小的体积,动画却能清晰流畅,还不用特意为做成循环妥协效果。


用「代码」作图

可能很多设计师都导出过静态的 SVG 资源给开发,工具方面我们可以在 Adobe Illustrator、Sketch 等工具中绘制好后直接导出,甚至我们打开记事本之类的文本编辑器都能手写一个。没错,SVG 就是一段段「代码」,通过浏览器读取并显示为图片或者动画。下面就是一段简单的 SVG 「代码」:

<svg width="1000" height="1000" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <circle cx="500" cy="350" r="300" fill="red"></circle>
  <text x="280" y="750" fill="blue" font-size="80">
    这是一个圆
  </text>
</svg>

将它复制到记事本或者其他文本编辑器, 保存为 .svg 格式,再用 Chrome 等浏览器打开,将能看到(根据操作系统不同字体可能不同):

不要被这些「代码」吓到,我们稍微修改一下,就能直观感受到它们对图像有怎样的影响。看这行:

<circle cx="500" cy="350" r="300" fill="red"></circle>

很明显这一行是控制那个圆的,而「cx」、「cy」控制圆的位置,「r」控制圆的半径,「fill」控制圆的填充颜色(可以用「red」,「blue」等单词表示颜色,但最好使用「#FF0000」这样的十六进制色值来精确控制)。

尝试改变这些值,保存,再在浏览器刷新查看,就能看到改动后的效果。

<circle cx="400" cy="550" r="100" fill="#00BFFF"></circle>

而圆只是基本图形之一,如果我们想画一条线,可以这么写:

<path stroke="red" stroke-width="5" d="M300,790 L700,790"></path>

把它加在 <circle> 后面

<circle …… ></circle>
<path stroke="red" stroke-width="5" d="M300,790 L700,790"></path>
<text ……

可以看到我们在「这是一个圆」这几个字下画了一条红线:

这行里,「stroke」、「stroke-width」分别控制线段的颜色和粗细,比较难懂的是「d="..."」里的内容。其实是这么个意思:

"M300,790 L700,790" = "Move to (300,790) Line to (700,790) ",也就是「从(300,790)这个位置,画一条直线到(700,790)」,表示位置的是坐标(x,y)。

这样我们就可以可以画两个圆,然后把他们用线连起来。

<circle cx="100" cy="550" r="50" fill="#00BFFF"></circle>
<circle cx="500" cy="350" r="50" fill="#00BFFF"></circle>
<path stroke="#00BFFF" stroke-width="5" d="M100,550 L500,350"></path>

OK,现在我们再回忆一下《刷脸生成你的12位图》项目里,当照片被扫描成功后出现在人脸上的图形,其实就是由这最基本的点和线组成。

那么这些点的位置是如何确定的呢?「黑科技」来自网易人工智能事业部,通过深度学习技术检测图片中人脸的面部特征点,返回每个特征点的坐标。通过计算这些坐标间的关系我们可以得出一些数据,「算命结果」就是根据这些数据进行差异化呈现。

感兴趣的可以到 网易人工智能官网的演示页面 来试试:


上传带人脸的照片,或者粘贴图片网址,点击「提交」,就会生成特征点。左边是在照片上显示点的位置,右边就是各个点的坐标等数据了。

如果扫描正常,点的数量是固定的,每个点编号如下:



设计师再在这个基础上做设计,根据编号细调每个点的颜色、粗细、透明度等。细心的你可能已经发现了,我们最终用到的点比实际可检测出的点要少,因为实际在手机上显示的时候人脸所占面积不大,不需要太多的点;但不知你有没有看出来,有的点并不是直接检测出来的,而是算出来的。

如下图,点69 是「点1和点2的连线」与「点40和点37的连线」的交点,点70 是「点17和点16的连线」与「点43和点46的连线」的交点,而 点71 是「点40和点43的连线」的中点。

为什么要计算这几个点?这样增加了设计的灵活度,更能直观的显示「下颌角」、「三庭五眼」之类的指标——我们「看相」还是很严谨的!

再看看另一个动画,从下巴扩散出去的绿线:

以其中一条线为例,从 点9(记作 p9)出发作一条过 点8(记作 p8)的「射线」,在射线上取一点 p0,令线段「p9-p8」 长度为 a,「p8-p0」长度为 b,由于 a 在每张图上是确定的,我们控制好 a:b 的比例 ,将 p9 和 p0 连起来,就能得到一条图中的绿线了,再如法炮制其它几组。这条线的比值是 a:b = 1:3 ,也就是 b 是 a 的3倍长度,其他的线根据视觉效果调整比例即可。

然后我们看看「五眼」——这个算起来就有点复杂了。分别过点1、点37、点40、点43、点46和点17作过「点69与点70连线」的垂线,这样就得到6个绿点。这些垂线的下端分别从点5、点6、点8、点10、点12、点13与之的垂线处截断,而上端则使用前一个图里从下巴扩散出去的绿线的算法,基于下端已知线段长度,通过比例控制向上延伸的程度。

有没有想起高考被数学支配的恐惧?幸好这不是考试,我们有万能的互联网!

我借助 WolframAlpha 网站解方程,你看出来是计算哪个部分的了吗?


让 SVG 动起来

好了,知道了图怎么「画」,下一步就让他们动起来!动画就是在元素中插入 animate 相关语句,例如:

<animate attributeName="opacity" from=" 1" to="0" begin="0s" dur="1s" repeatCount="indefinite"></animate>

就是在 1 秒内让这个元素的不透明度从 1 变为 0,并一直重复这个动画。

还有一种写法是:

<animate atrribute="opacity" values="1;0" begin="0s" dur="1s"  repeatCount="indefinite"></animate>

这里「values="1;0"」等价于「from="1" to="0"」,但「from…to」方式只能指定初始和结束状态的值,「values」方式能有更多变化,比如我们写成「values="1;0;1"」就是在「1秒内」让这个元素的「不透明度」从 1 变为 0 再变回 1,完成一个循环。如果写成「values="1;0;1;0;1;0;1;0;1;0;1"」就是一个闪烁动画。

这一段「代码」应该放到哪里呢?

回顾一下我们的 SVG 「代码」,可以发现他是这么个结构:

<svg aaa="bbb" ccc="ddd"……>
  <xx元素 eee="fff" ……></>
  <yy元素 ggg="hhh" ……></>
</svg>

这里用的是「XML」语法,其中以 < 开头,以 > 结尾的东西是一个「标签」。如果是 <……> 这种格式的是「开始标签」; </……> 是「结束标签」;还有一种 <……/> 是「空元素标签」。

每个「开始标签」后都必须跟一个「结束标签」,开始标签和结束标签之间可以插入内容, <……>内容</……> ,内容可以是文字(例如例子里的「这是一个圆」)也可以是其他标签,或则混合在一起。每一个组「开始标签」「结束标签」加上其中的内容我们称之为一个「元素」,如果内容为空,<……></……> 可以写成 <……/> 即「空元素标签」,一个空元素标签就是一个「元素」。

所以例子里的

<circle cx="500" cy="350" r="300" fill="red"></circle>

可以写成

<circle cx="500" cy="350" r="300" fill="red"/>

而标签里的「xx="yy"」,其中「xx」是该元素的一个「属性」,「yy」是他的值,每个属性要么不写出来(不写出来表示使用默认值),写了必须有「=」跟一个值,值必须用引号扩起来,多个属性之间用空格隔开。在 SVG 里什么元素有什么属性,属性值格式都是规定好的,在这个例子里我们只需要关注几个简单的就好。

回到动画问题,我们需要做的就是把动画元素作为 <circle> 元素的内容插入

<circle cx="100" cy="550" r="50" fill="#00BFFF">
  <animate attributeName="opacity" values="1;0" begin="0s" dur="1s"  repeatCount="indefinite"/>
</circle>

这样就是让这个圆做不透明度从 1 到 0 渐隐消失的动画了。

这个例子里为了让效果明显我将动画元素里的「repeatCount」属性设为「"indefinite"」即让它每次播放完再从头播放,有时候会希望它只播放一遍,这时如果我们把「repeatCount」属性设为「"1"」,会发现虽然动画只播放一次了,却没有保持最后一刻的样子,而是跳到了动画播放前的状态。此时我们需要再设置一个属性「fill」令「fill="freeze"」,即:

<animate attributeName="opacity" values="1;0" begin="0s" dur="1s"  repeatCount="1" fill="freeze"/>

就能达到目的了。

另外动画元素里有个属性「begin="0s"」表示动画立即开始,如果是「begin="1s"」就是动画延迟 1 秒再开始,用这个参数我们可以控制多个动画的顺序,比如让几个圆点依次消失:

透明度的动画我们搞定了,接下来看看线条生长的动画。同样道理,我们把「attributeName」属性设为「"d"」,「values」设为初始和结束时的路径就好。

<animate attributeName="d" values="M100,550 L100,550;M100,550 L500,350" begin="0s" dur="1s" repeatCount="1" fill="freeze"/>

然后插入对应的 <path> 元素中就好。

多说一句,对于直线我们可以这么写,如果路径是一条折线或者曲线就没那么简单了,限于篇幅这里不展开,感兴趣可以看看文末的链接。现在我们综合一下以上提到的知识,做一个「点闪烁出现 -> 线条生长 -> 另外一个点出现」的动画:

<svg width="1000" height="1000" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <circle cx="100" cy="550" r="50" fill="#00BFFF" opacity="0">
        <animate attributeName="opacity" values="0;1;0;1;0;1;0;1" begin="0s" dur="0.5s" repeatCount="1" fill="freeze"/>
    </circle>
    <path stroke="#00BFFF" stroke-width="5" d="M100,550 L100,550">
        <animate attributeName="d" values="M100,550 L100,550;M100,550 L500,350" begin="0.5s" dur="0.5s" repeatCount="1" fill="freeze"/>
    </path>
    <circle cx="500" cy="350" r="50" fill="#00BFFF" opacity="0">
        <animate attributeName="opacity" values="0;1;0;1;0;1;0;1" begin="1s" dur="0.5s" repeatCount="1" fill="freeze"/>
    </circle>
</svg>

注意我将每个「需要做动画的元素」里的「需要做动画的属性」的值都设置为动画开始前的状态,比如<circle>元素都加上了「opacity="0"」,这样就能保证动画开始前整个画面是空白的。

好了,点和线的动画都搞定,我们就可以让人脸上的特征点动起来了。动画的原理都很基础,并不复杂,需要的是一点点耐心,和不断的刷新测试,估计时间和节奏,仔细打磨。

这样针对一张照片的动画就基本完工了,但是每个人上传的照片是不一样的,怎么保证动画能对上位置呢?所以最终我做的是一个「SVG 动画生成器」,通过获取你上传的照片所返回的特征点位置数据,再生成位置匹配的 SVG 动画代码,嵌入到 H5 中。

我做了个简陋的网站调用它,感兴趣的可以扫下图的码或者 点击这里体验一下




使用设计软件制作 SVG 动画

前面说的都是直接用「代码」制作动画,但是对于我们设计师来说更友好的方式还是用软件设计好静态视觉元素,再添加点「代码」让它动起来。不过要读懂并修改这些导出的「代码」也是得花一点功夫的哦。我们来看看「点击拍照」和「文字跑马灯」这俩例子:

先说说「跑马灯」,我是用 Sketch 把字排起来的(为了保证各个端字体一致我将文本转曲了),然后画了个遮罩,遮掉蓝字部分,再将整个画板导出为 SVG 。

我导出的时候没有隐藏遮罩图层,因为隐藏了遮罩就没了……不知道算不算 Sketch 的小 bug 。没关系,我们可以直接调整 SVG 代码。

首先有一个要注意的点,请看我们之前章节写的「代码」的 <svg> 标签那一行:

<svg width="1000" height="1000" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg">

最后的「xmlns="w3.org/2000/svg"」看起来像是个「超链接」,实际上只是个「定义」,告诉浏览器这里的「代码」遵循 SVG 标准,否则浏览器会无法正确读取 SVG。

另外顺便提一下,这里的「width」、「height」表示宽、高,「viewBox」指定的是「可视区域」,「"0 0 1000 1000"」,意思是可视区域为以左上角(坐标 [0,0])到右下角(坐标 [1000,1000])为对角线的矩形区域,我们现在的数值是和 SVG 图像的宽高一致,如果可视区域小于图像宽高,或者对角线划过的区域不吻合,多出的部分将会被切掉看不见。然后是「version」是指的 SVG 标准的版本,现在是 1.1 版。

回到上一张图里,由 Sketch 导出的 SVG 代码中,<svg> 标签中又多了个属性「xmlns:xlink = "w3.org/1999/xlink"」这个设置开启了 引用 功能。

我们看看图中的第 6、7、8 行:

<defs>
    <polygon id="path-1" points="99 839 0 839 0 146 99 146 83.5898438 104.871094 83.5898438 27.8964844 99 0 742 0 742 996 99 996 79.4570312 976.712891 79.4570312 887.115234"></polygon>
</defs>

<defs> 标签有点像 Sketch 的 Symbol,标签下的子元素可以被重复引用,并且如果没有被引用,是不会出现在画面里的(不过 SVG 还有个 <symbol> 标签,比 <defs> 能做更多设置,但是这里没出现,就不深入介绍了)。

这个 <polygon> 就是我画的那个不规则遮罩,注意标签里有个属性「id="path-1"」,这个「id」的值在整个 SVG 里是 唯一 的,就像我们的身份证一样。

再往下看到 10、11、12 行

<mask id="mask-2" fill="white">
    <use xlink:href="#path-1"></use>
</mask>

<mask> 标签下的就是遮罩元素了,属性里「id="mask-2"」指定了它自己的 id,而「fill="white"」则是由于之前 <defs> 里的 <polygon> 只定义了路径没定义填充色所以在这里设置,对于 mask 来说,白色是完全显示所遮罩的内容,如果写成「fill="black"」就是什么都看不见了,如果换成别的颜色比如 red 就是半透明。如果是渐变,就能做出有透明度变化的遮罩,如果是一张照片……哈哈这里不深入展开了,感兴趣可以用搜索引擎搜索 SVG mask 了解一下。

中间的 <use> 就是引用了之前的 <defs> 里定义的 id 为「path-1」的图形。注意写法「xlink:href="# + 引用的元素的 id +"」。

不过光有这句还不够哦,这里只是 定义 了一个遮罩元素,遮哪里还需要对应的代码配合才行。

其实如果们不用引用的方式,删掉整个 <defs> ,直接把 <polygon> 元素放在 <mask> 下也是 OK 的,还更直观。不过既然 Sketch 是这么导出的,我们就按它的思路走,顺便了解一下 <use> 的用法也不错。

<mask id="mask-2" fill="white">
    <polygon id="path-1" points="99 839 0 839 0 146 99 146 83.5898438 104.871094 83.5898438 27.8964844 99 0 742 0 742 996 99 996 79.4570312 976.712891 79.4570312 887.115234"></polygon>
</mask>

接下来看看第 13 行:

<use id="textmask" fill="#D8D8D8" xlink:href="#path-1"></use>

这个 id 「textmask」是不是有点眼熟?看上面的 Sketch 文件界面截图,最下面的遮罩图层名就是「textmask」。没错 Sketch 导出 SVG 的时候会把图层名设置为对应元素的 id ,如果同名就在后面加个「-1」、「-2」等。另外我们可以看到在 Sketch 的图层顺序里「textmask」是在最下方的,SVG 中却最先出现了,顺序是反过来的哦!

之前说过 Sketch 导出 SVG 的时候为了带上遮罩我没有隐藏遮罩图层,现在既然找到它了,哼哼,当然是直接删掉或者注释掉了。所谓注释,就是将这一行不做为代码处理,在 SVG 里就是在要注释的行前后加上「<!-- 」和「 -->」

<!-- <use id="textmask" fill="#D8D8D8" xlink:href="#path-1"></use> -->

这样我们的图就完美了。

然而遮罩的问题才刚开始……前面只提到了遮罩的定义,现在来说说怎么用。我们用文本编辑器里的「搜索」功能直接搜图层名,比如「正脸」,就能定位到该图层 id 所在:

旁边就是遮罩 mask 的用法:

<id="正脸" …… mask="url(# + 遮罩图层的 id + )">

这样这个图层就被遮罩图层遮住了,其他几个图层也是如此。不过这里提醒一下,虽然 id 可以用中文,但是在实际应用中最好使用 英文+数字,且不要带上空格,不然会有乱码出错的风险。

总结一下遮罩的用法:

  1. 创建或者设置 <mask> 元素
  2. 在需要引用的元素标签里加上 「mask="url(# + 遮罩图层的 id + )"」

简单分析了 Sketch 导出的代码,接下来可以开始做动画了!先说说动画的设计思路,其实就是保持遮罩不动,文字图层水平或者垂直位移,由于文字是重复的,只需位移到「重合点」,再循环就好了。

之前的章节提到了 <animate> 标签,这次我们用个新的 —— <animateTransform> ,看名字就知道它是专注于「变换动画」的标签,可以为这些属性做动画:

  • 位移(translate)
  • 旋转(rotate)
  • 缩放(scale)
  • 倾斜 (skewX、skewY)

其写法是(为了解释方便我将属性竖着排列了,注意每个属性之间有空格):

<animateTransform 
    attributeName="transform"
    type="translate"
    from="0 0"
    to="-132.96 0"
    begin="0s"
    dur="1s"
    repeatCount="indefinite"
/>

后边的属性基本和 <animate> 一样,主要是前面的「attributeName = "transform"」和「type = "translate"」,这里「type」属性指定了变换类型,「translate」就是位移动画,[0 0]、[-132.96 0] 是坐标 [x y],为什么是「-132.96」?当然是设计稿里量出来的啦,移动这么多距离正好和移动前重合。

这里再扩展一个小技巧。我们之前的做法是将 <animate> 插入到需要动画的元素中间作为其子元素,如果动画很多,动画相关的代码就会分散于很多地方难以修改。而前面提到了「引用」,其实动画也是可以「引用」的,只不过是把动画自己「引」到目标上。做法也很简单,在动画标签里插入「xlink:href="# + 引用的元素的 id +" 」

<animateTransform 
        xlink:href="#正脸" 
        attributeName="transform"
        type="translate"
        from="0 0"
        to="-132.96 0"
        begin="0s"
        dur="1s"
        repeatCount="indefinite"
/>

总结一下「xmlns:xlink」引用的用法:

  1. <svg> 标签中引入属性「xmlns:xlink = "w3.org/1999/xlink"」
  2. 在需要引用的地方设置 「xlink:href="# + 引用的元素的 id +" 」

这样我们就可以跳出标签,把动画集中在一个地方方便修改了。

OK 我们来预览一下动画:

好像哪里不对?是的,我们期望的是 遮罩不动字在动,现在是遮罩和字一起动了!怎么回事呢?仔细想想,我们的遮罩是和文字图层在一个标签里的:

所以动画其实没毛病!找到病因,我们要做的就是对症下药:把遮罩从每个文字元素里去掉,然后把他们放进一个「组」里,再对这个组做遮罩,而动画的目标是阻里的文字层,组是不动的,这样就能达到目的了!

SVG 里分组很简单,用 <g></g> 标签把他们括起来就行。

别忘了最后加上结束标签 </g>

好,现在再看看动画,完美!

对其他几组字再如法炮制,我们的跑马灯就完成了!

然后我们再看「点击拍照」。这个动画分两部分:底下斑马线的循环移动,和按钮的按下及回弹。首先看看设计稿:

「斑马线」就是用很多条黑线排列,然一起旋转,最后用一个方形遮罩让它只露出方块内部的部分。我们用「文字跑马灯」动画的做法让它循环位移动画就好。

然后是按钮的按下动画,也很简单,就是将「点击拍照」四个字、白色方块,三根黑线作为一个组,对这个组进行位移动画,而「按下」的时候三根黑线会超出这个 SVG 的「可视区域」范围而被裁切掉。

  <animateTransform 
          xlink:href="#djpz" 
          attributeType="XML"
          attributeName="transform"
          type="translate"
          values="0 0;5 5;0 0;0 0"
          keyTimes="0;0.02;0.2;1"
          begin="0s"
          dur="2s"
          repeatCount="indefinite"
  />

这里还有个属性「keyTimes」。默认情况下我们如果不设置「keyTimes」,「values」中的值变化到下一个值的时间是固定的:比如动画总时长2秒,「values」 里有四个值「"0 0;5 5;0 0;0 0"」,那么「0 0」到「5 5」、「5 5」到「0 0」、「0 0」到「0 0」用时都是 2/4 = 0.5 秒(小技巧:此处设置一个 「0 0」到「0 0」 是为了制造一个时间间隔);现在指定了「keyTimes = "0;0.02;0.2;1"」,那么每段所用时间就变为:

  • 「0 0」到「5 5」用时 2 x (0.02 - 0) = 0.04 秒
  • 「5 5」到「0 0」用时 2 x (0.2 - 0.02) = 0.36 秒
  • 「0 0」到「0 0」用时 2 x (1 - 0.2) = 1.6 秒

这样就完成了「快速按下,再慢慢弹起,然后间隔一段时间,再重复按下」的动画。

这两个动画的 SVG 文件和 Sketch 源文件可以点击这里下载

最后再聊聊首页那个大卫石膏模型的动画:

这个就不是 SVG 动画了,是我用 AE 制作后,通过自己写的 AE 脚本「CSS Sprite Exporter 」导出的 CSS 精灵图动画。关于这个可以看看我之前的一篇文章介绍,我觉得在 H5 项目里还是挺实用的。


结束语

SVG 非常博大精深,可以说是 web 端 2D 动画利器,作为设计师,可能会因为对「代码」有恐惧心里而不敢碰,其实没有那么难,多看资料,多多尝试,多多思考,它会成为你一个从更多维度展现设计想法的绝妙途径哦。


本文写作过程中参考了以下文章和资料:
w3.org/TR/SVG11/
zhangxinxu.com/wordpres
oxxostudio.tw/articles/

编辑于 2018-07-17

文章被以下专栏收录