This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
<audio id="audio" title="01 | 浏览器中实现可视化的四种方式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/df/a1/dfc675382eef454f1dc612d18ffdeaa1.mp3"></audio>
你好,我是月影。
上一节课我们了解了什么是可视化。可视化用一句话来说本质上就是将数据信息组织起来后以图形的方式呈现出来。在Web上图形通常是通过浏览器绘制的。现代浏览器是一个复杂的系统其中负责绘制图形的部分是渲染引擎。渲染引擎绘制图形的方式我总结了一下大体上有4种。
第1种是传统的**HTML+CSS**。这种方式通常用来呈现普通的Web网页。
第2种是使用**SVG**。SVG和传统的**HTML+CSS的绘图方式差别不大**。只不过HTML元素在绘制矢量图形方面的能力有些不足我们后面会讲到而SVG恰好弥补了这方面的缺陷。
第3种是使用**Canvas2D**。这是浏览器提供的Canvas API中的其中一种上下文使用它可以非常方便地绘制出基础的几何图形。在可视化中Canvas比较常用下一节课我们会学习它的基本用法。
第4种是使用**WebGL**。这是浏览器提供的Canvas API中的另一种上下文它是OpenGL ES规范在Web端的实现。我们可以通过它用GPU渲染各种复杂的2D和3D图形。值得一提的是WebGL利用了GPU并行处理的特性这让它在处理大量数据展现的时候性能大大优于前3种绘图方式。因此在可视化的应用中一些数据量大、视觉效果要求高的特殊场景使用WebGL渲染是一种比较合适的选择。
这4种方式各有利弊今天我就从宏观层面带你了解这些图形系统为我们后面更深入的学习打好基础。
## 方式一HTML+CSS
与传统的Web应用相比可视化项目尤其是PC端的可视化大屏展现不只是使用HTML与CSS相对较少而且使用方式也不太一样。于是有些同学就会认为可视化只能使用SVG、Canvas这些方式不能使用HTML与CSS。当然了这个想法是不对。具体的原因是什么呢我一起来看看。
实际上现代浏览器的HTML、CSS表现能力很强大完全可以实现常规的图表展现比如我们常见的柱状图、饼图和折线图。
虽然我们后面的课程会主要使用Canvas和WebGL绘图少数会涉及部分CSS。但是你可不要觉得它不重要。为啥呢理由有两个
- 一些简单的可视化图表用CSS来实现很有好处既能简化开发又不需要引入额外的库可以节省资源提高网页打开的速度。
- 理解CSS的绘图思想对于可视化也是很有帮助的比如CSS的很多理论就和视觉相关可视化中都可以拿来借鉴。
所以呢这一节里我们多讲一点你一定要好好听。接下来我们就来说一说CSS是如何实现常规图表的。
### 1. HTML与CSS是如何实现可视化的
用CSS实现柱状图其实很简单原理就是使用网格布局Grid Layout加上线性渐变Linear-gradient我就不多说了你可以直接看我这里给出的CSS代码。
<img src="https://static001.geekbang.org/resource/image/68/31/68d43be360923664f2a3d8c2c65fbc31.jpg" alt="" title="用HTML+CSS绘制的柱状图">
```
/**
dataset = {
current: [15, 11, 17, 25, 37],
total: [25, 26, 40, 45, 68],
}
*/
.bargraph {
display: grid;
width: 150px;
height: 100px;
padding: 10px;
transform: scaleY(3);
grid-template-columns: repeat(5, 20%);
}
.bargraph div {
margin: 0 2px;
}
.bargraph div:nth-child(1) {
background: linear-gradient(to bottom, transparent 75%, #37c 0, #37c 85%, #3c7 0);
}
.bargraph div:nth-child(2) {
background: linear-gradient(to bottom, transparent 74%, #37c 0, #37c 89%, #3c7 0);
}
.bargraph div:nth-child(3) {
background: linear-gradient(to bottom, transparent 60%, #37c 0, #37c 83%, #3c7 0);
}
.bargraph div:nth-child(4) {
background: linear-gradient(to bottom, transparent 55%, #37c 0, #37c 75%, #3c7 0);
}
.bargraph div:nth-child(5) {
background: linear-gradient(to bottom, transparent 32%, #37c 0, #37c 63%, #3c7 0);
}
```
而要实现饼图,我们可以使用圆锥渐变,方法也很简单,你直接看代码就可以理解。
<img src="https://static001.geekbang.org/resource/image/58/6d/58c6ea6ffce4e47446c0c9636d47226d.jpg" alt="" title="使用圆锥渐变绘制的饼图">
```
.piegraph {
display: inline-block;
width: 250px;
height: 250px;
border-radius: 50%;
background-image: conic-gradient(#37c 30deg, #3c7 30deg, #3c7 65deg, orange 65deg, orange 110deg, #f73 110deg, #f73 200deg, #ccc 200deg);
}
```
柱状图和饼图都比较简单所以我带你快速过了一下。除此之外我们用HTML和CSS也可以实现折线图。
我们可以用高度很小的Div元素来模拟线段然后用transform改变角度和位置这样就能拼成折线图了。 另外如果使用clip-path这样的高级属性我们还能实现更复杂的图表比如用不同的颜色表示两个不同折线的面积。
<img src="https://static001.geekbang.org/resource/image/cc/c9/cc4d0f6d9260d508758c8043a14ea1c9.jpg" alt="" title="折线图和面积图">
实际上很多常见的可视化图表我们都可以用HTML和CSS来实现不需要用其他的绘图方式。但是为什么在可视化领域很少有人直接用HTML和CSS来绘制图表呢这主要是因为使用HTML和CSS绘图有2个缺点。
### 2.用HTML+CSS实现可视化的缺点
首先HTML和CSS主要还是为网页布局而创造的使用它们虽然能绘制可视化图表但是绘制的方式并不简洁。这是因为从CSS代码里我们很难看出数据与图形的对应关系有很多换算也需要开发人员自己来做。这样一来一旦图表或数据发生改动就需要我们重新计算维护起来会很麻烦。
其次HTML和CSS作为浏览器渲染引擎的一部分为了完成页面渲染的工作除了绘制图形外还要做很多额外的工作。比如说浏览器的渲染引擎在工作时要先解析HTML、SVG、CSS构建DOM树、RenderObject树和RenderLayer树然后用HTML或SVG绘图。当图形发生变化时我们很可能要重新执行全部的工作这样的性能开销是非常大的。
而且传统的Web开发因为涉及UI构建和内容组织所以这些额外的解析和构建工作都是必须做的。而可视化与传统网页不同它不太需要复杂的布局更多的工作是在绘图和数据计算。所以对于可视化来说这些额外的工作反而相当于白白消耗了性能。
因此相比于HTML和CSSCanvas2D和WebGL更适合去做可视化这一领域的绘图工作。它们的绘图API能够直接操作绘图上下文一般不涉及引擎的其他部分在重绘图像时也不会发生重新解析文档和构建结构的过程开销要小很多。
<img src="https://static001.geekbang.org/resource/image/d4/9d/d49d2fb673a7fb9f8de329c12fab009d.jpg" alt="" title="图形系统与浏览器渲染引擎工作对比">
## 方式二SVG
在介绍Canvas2D和WebGL之前我们先来说一说SVG。现代浏览器支持SVGScalable Vector Graphics可缩放矢量图SVG是一种基于 XML 语法的图像格式可以用图片img元素的src属性加载。而且浏览器更强大的是它还可以内嵌SVG标签并且像操作普通的HTML元素一样利用DOM API操作SVG元素。甚至CSS也可以作用于内嵌的SVG元素。
比如上面的柱状图如果用SVG实现的话我们可以用如下所示的代码来实现
```
&lt;!--
dataset = {
total: [25, 26, 40, 45, 68],
current: [15, 11, 17, 25, 37],
}
--&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;120px&quot; height=&quot;240px&quot; viewBox=&quot;0 0 60 100&quot;&gt;
&lt;g transform=&quot;translate(0, 100) scale(1, -1)&quot;&gt;
&lt;g&gt;
&lt;rect x=&quot;1&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;25&quot; fill=&quot;#37c&quot;/&gt;
&lt;rect x=&quot;13&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;26&quot; fill=&quot;#37c&quot;/&gt;
&lt;rect x=&quot;25&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;40&quot; fill=&quot;#37c&quot;/&gt;
&lt;rect x=&quot;37&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;45&quot; fill=&quot;#37c&quot;/&gt;
&lt;rect x=&quot;49&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;68&quot; fill=&quot;#37c&quot;/&gt;
&lt;/g&gt;
&lt;g&gt;
&lt;rect x=&quot;1&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;15&quot; fill=&quot;#3c7&quot;/&gt;
&lt;rect x=&quot;13&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;11&quot; fill=&quot;#3c7&quot;/&gt;
&lt;rect x=&quot;25&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;17&quot; fill=&quot;#3c7&quot;/&gt;
&lt;rect x=&quot;37&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;25&quot; fill=&quot;#3c7&quot;/&gt;
&lt;rect x=&quot;49&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;37&quot; fill=&quot;#3c7&quot;/&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;/svg&gt;
```
从上面的SVG代码中我们可以一目了然地看出数据total和current分别对应SVG中两个g元素下的rect元素的高度。也就是说元素的属性和数值可以直接对应起来。而CSS代码并不能直观体现出数据的数值需要进行CSS规则转换。具体如下图所示
<img src="https://static001.geekbang.org/resource/image/c9/62/c9716f8c3768a2384c3baf5f8ec87362.jpg" alt="">
在上面这段SVG代码中g表示分组rect表示绘制一个矩形元素。除了rect外SVG还提供了丰富的图形元素可以绘制矩形、圆弧、椭圆、多边形和贝塞尔曲线等等。由于SVG比较复杂我们会在第4节课专门介绍如何用SVG绘制可视化图表。在那之前你也可以通过[MDN官方文档](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial)来学习更多的SVG的API。
SVG绘制图表与HTML和CSS绘制图表的方式差别不大只不过是将HTML标签替换成SVG标签运用了一些SVG支持的特殊属性。
HTML的不足之处在于HTML元素的形状一般是矩形虽然用CSS辅助也能够绘制出各种其它形状的图形甚至不规则图形但是总体而言还是非常麻烦的。而SVG则弥补了这方面的不足让不规则图形的绘制变得更简单了。因此用SVG绘图比用HTML和CSS要便利得多。
但是SVG图表也有缺点。在渲染引擎中SVG元素和HTML元素一样在输出图形前都需要经过引擎的解析、布局计算和渲染树生成。而且一个SVG元素只表示一种基本图形如果展示的数据很复杂生成图形的SVG元素就会很多。这样一来大量的SVG元素不仅会占用很多内存空间还会增加引擎、布局计算和渲染树生成的开销降低性能减慢渲染速度。这也就注定了SVG只适合应用于元素较少的简单可视化场景。
## 方式三Canvas2D
除了SVG使用Canvas2D上下文来绘制可视化图表也很方便但是在绘制方式上Canvas2D和HTML/CSS、SVG又有些不同。
无论是使用HTML/CSS还是SVG它们都属于**声明式**绘图系统也就是我们根据数据创建各种不同的图形元素或者CSS规则然后利用浏览器渲染引擎解析它们并渲染出来。但是Canvas2D不同它是浏览器提供的一种可以直接用代码在一块平面的“画布”上绘制图形的API使用它来绘图更像是传统的“编写代码”简单来说就是调用绘图指令然后引擎直接在页面上绘制图形。这是一种**指令式**的绘图系统。
那Canvas到底是怎么绘制可视化图表的呢我们一起来看。
首先Canvas元素在浏览器上创造一个空白的画布通过提供渲染上下文赋予我们绘制内容的能力。然后我们只需要调用渲染上下文设置各种属性然后调用绘图指令完成输出就能在画布上呈现各种各样的图形了。
为了实现更加复杂的效果Canvas还提供了非常丰富的设置和绘图API我们可以通过操作上下文来改变填充和描边颜色对画布进行几何变换调用各种绘图指令然后将绘制的图形输出到画布上。
总结来说Canvas能够直接操作绘图上下文不需要经过HTML、CSS解析、构建渲染树、布局等一系列操作。因此单纯绘图的话Canvas比HTML/CSS和SVG要快得多。
但是因为HTML和SVG一个元素对应一个基本图形所以我们可以很方便地操作它们比如在柱状图的某个柱子上注册点击事件。而同样的功能在Canvas上就比较难实现了因为对于Canvas来说绘制整个柱状图的过程就是一系列指令的执行过程其中并没有区分“A柱子”、“B柱子”这让我们很难单独对Canvas绘图的局部进行控制。不过这并不代表我们就不能控制Canvas的局部了。实际上通过数学计算我们是可以通过定位的方式来获取局部图形的在后续的课程中我们会解决这个问题。
这里有一点需要你注意Canvas和SVG的使用也不是非此即彼的它们可以结合使用。因为SVG作为一种图形格式也可以作为image元素绘制到Canvas中。举个例子我们可以先使用SVG生成某些图形然后用Canvas来渲染。这样我们就既可以享受SVG的便利性又可以享受Canvas的高性能了。
## 方式四WebGL
WebGL绘制比前三种方式要复杂一些因为WebGL是基于OpenGL ES规范的浏览器实现的API相对更底层使用起来不如前三种那么简单直接。关于WebGL的使用内容我会在3D篇详细来说。
一般情况下Canvas2D绘制图形的性能已经足够高了但是在三种情况下我们有必要直接操作更强大的GPU来实现绘图。
第一种情况,如果我们**要绘制的图形数量非常多**比如有多达数万个几何图形需要绘制而且它们的位置和方向都在不停地变化那我们即使用Canvas2D绘制了性能还是会达到瓶颈。这个时候我们就需要使用GPU能力直接用WebGL来绘制。
第二种情况,如果我们要**对较大图像的细节做像素处理**比如实现物体的光影、流体效果和一些复杂的像素滤镜。由于这些效果往往要精准地改变一个图像全局或局部区域的所有像素点要计算的像素点数量非常的多一般是数十万甚至上百万数量级的。这时即使采用Canvas2D操作也会达到性能瓶颈所以我们也要用WebGL来绘制。
第三种情况是**绘制3D物体**。因为WebGL内置了对3D物体的投影、深度检测等特性所以用它来渲染3D物体就不需要我们自己对坐标做底层的处理了。那在这种情况下WebGL无论是在使用上还是性能上都有很大优势。
## 要点总结
今天,我们介绍了四种可视化实现方式和它们的优缺点。
HTML+CSS的优点是方便不需要第三方依赖甚至不需要JavaScript代码。如果我们要绘制少量常见的图表可以直接采用HTML和CSS。它的缺点是CSS属性不能直观体现数据绘制起来也相对麻烦图形复杂会导致HTML元素多而消耗性能。
SVG 是对HTML/CSS的增强弥补了HTML绘制不规则图形的能力。它通过属性设置图形可以直观地体现数据使用起来非常方便。但是SVG也有和HTML/CSS同样的问题图形复杂时需要的SVG元素太多也非常消耗性能。
Canvas2D 是浏览器提供的简便快捷的指令式图形系统它通过一些简单的指令就能快速绘制出复杂的图形。由于它直接操作绘图上下文因此没有HTML/CSS和SVG绘图因为元素多导致消耗性能的问题性能要比前两者快得多。但是如果要绘制的图形太多或者处理大量的像素计算时Canvas2D依然会遇到性能瓶颈。
WebGL 是浏览器提供的功能强大的绘图系统它使用比较复杂但是功能强大能够充分利用GPU并行计算的能力来快速、精准地操作图像的像素在同一时间完成数十万或数百万次计算。另外它还内置了对3D物体的投影、深度检测等处理这让它更适合绘制3D场景。
知道了这些优缺点,在实际面对可视化需求的时候,我们就可以根据具体项目的特点来选择合适的方案实现可视化了。那具体来说,我们应该怎么选择呢?我这里梳理了一张技术方案的选择图,你可以看一看。
<img src="https://static001.geekbang.org/resource/image/3b/6f/3bf11fcf520504a4e342dd335698c76f.jpg" alt="">
## 小试牛刀
我们在文中实现了SVG版本的柱状图你可以尝试用SVG实现同HTML/CSS版本一样的饼图、折线图和面积图体会一下使用SVG实现和HTML/CSS实现的不同点。
另外下一节课我们会介绍Canvas2D绘制可视化图表你可以提前预习一下试一试能否用Canvas2D来绘制文中的柱状图。
欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课见!

View File

@@ -0,0 +1,364 @@
<audio id="audio" title="02 | 指令式绘图系统如何用Canvas绘制层次关系图" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/09/96/09058a18fbecb3d2d69c14466ec8ae96.mp3"></audio>
你好,我是月影。
Canvas是可视化领域里非常重要且常用的图形系统在可视化项目中它能够帮助我们将数据内容以几何图形的形式非常方便地呈现出来。
今天我们就在上一节课的基础上对Canvas进行稍微深入一些的介绍来学习一下Canvas绘制基本几何图形的方法。
我主要会讲解如何用它的2D上下文来完成绘图不过我不会去讲和它有关的所有Api重点只是希望通过调用一些常用的API能给你讲清楚Canvas2D能做什么、要怎么使用以及它的局限性是什么。最后我还会带你用Canvas绘制一个表示省市关系的层次关系图Hierarchy Graph。希望通过这个可视化的例子能帮你实践并掌握Canvas的用法。
在我们后面的课程中基本上70~80%的图都可以用Canvas来绘制所以其重要性不言而喻。话不多说让我们正式开始今天的内容吧
## 如何用Canvas绘制几何图形
首先我们通过一个绘制红色正方形的简单例子来讲一讲Canvas的基本用法。
### 1. Canvas元素和2D上下文
对浏览器来说Canvas也是HTML元素我们可以用canvas标签将它插入到HTML内容中。比如我们可以在body里插入一个宽、高分别为512的canvas元素。
```
&lt;body&gt;
&lt;canvas width=&quot;512&quot; height=&quot;512&quot;&gt;&lt;/canvas&gt;
&lt;/body&gt;
```
这里有一点需要特别注意Canvas元素上的width和height属性不等同于Canvas元素的CSS样式的属性。这是因为CSS属性中的宽高影响Canvas在页面上呈现的大小而HTML属性中的宽高则决定了Canvas的坐标系。为了区分它们我们称Canvas的HTML属性宽高为**画布宽高**CSS样式宽高为**样式宽高**。
在实际绘制的时候如果我们不设置Canvas元素的样式那么Canvas元素的画布宽高就会等于它的样式宽高的像素值也就是512px。
而如果这个时候我们通过CSS设置其他的值指定了它的样式宽高。比如说我们将样式宽高设置成256px那么它实际的画布宽高就是样式宽高的两倍了。代码和效果如下
```
canvas {
width: 256px;
height: 256px;
}
```
<img src="https://static001.geekbang.org/resource/image/01/f1/01b8b4f686e776b379a2d8141ac13ff1.jpg" alt="" title="将canvas的样式宽高设为256px画布宽高设为512px">
因为画布宽高决定了可视区域的坐标范围所以Canvas将画布宽高和样式宽高分开的做法能更方便地适配不同的显示设备。
比如我们要在画布宽高为500`*`500的Canvas画布上绘制一个居中显示的100`*`100宽高的正方形。我们只要将它的坐标设置在 x = 200, y = 200 处即可。这样不论这个Canvas以多大的尺寸显示在各种设备上我们的代码都不需要修改。否则如果Canvas的坐标范围画布宽高跟着样式宽高变化那么当屏幕尺寸改变的时候我们就要重新计算需要绘制的图形的所有坐标这对于我们来说将会是一场“灾难”。
<img src="https://static001.geekbang.org/resource/image/5e/45/5e5d78e4fd8c42948b6ae68e79e69e45.jpg" alt="" title="在画布宽高为500*500的Canvas上居中绘制一个100*100的正方形">
### 2. Canvas 的坐标系
好了Canvas画布已经设置好了接下来我们来了解一下Canvas的坐标系。
Canvas的坐标系和浏览器窗口的坐标系类似它们都默认左上角为坐标原点x轴水平向右y轴垂直向下。那在我们设置好的画布宽高为512`*`512的Canvas画布中它的左上角坐标值为0,0右下角坐标值为512,512 。这意味着坐标0,0512,512之间的所有图形都会被浏览器渲染到画布上。
[<img src="https://static001.geekbang.org/resource/image/6c/f1/6cb9eca4e9c8c64a5e07694f7f6428f1.jpg" alt="" title="canvas坐标系">](https://www.jianshu.com/p/ef8244cb7ec4)
注意上图中这个坐标系的y轴向下意味着这个坐标系和笛卡尔坐标系不同它们的y轴是相反的。那在实际应用的时候如果我们想绘制一个向右上平抛小球的动画它的抛物线轨迹在Canvas上绘制出来的方向就是向下凹的。
[<img src="https://static001.geekbang.org/resource/image/f0/2e/f025b55279ea553387a058ab9184012e.jpeg" alt="" title="Canvas坐标系">](https://www.jianshu.com/p/ef8244cb7ec4)
另外,如果我们再考虑旋转或者三维运动,这个坐标系就会变成“左手系”。而左手系的平面法向量的方向和旋转方向,和我们熟悉的右手系相反。如果你现在还不能完全理解它们的区别,那也没关系,在实际应用的时候,我会再讲的,这里你只需要有一个大体印象就可以了。
[<img src="https://static001.geekbang.org/resource/image/2c/0d/2c01d99b4a3c9a46ba446cc208a6560d.jpeg" alt="" title="左手系和右手系">](https://zhuanlan.zhihu.com/p/64707259)
### 3. 利用 Canvas 绘制几何图形
有了坐标系我们就可以将几何图形绘制到Canvas上了。具体的步骤可以分为两步分别是获取Canvas上下文和利用Canvas 上下文绘制图形。下面,我们一一来看。
**第一步获取Canvas上下文。**
那在JavaScript中我们要获取Canvas上下文也需要两个步骤。首先是获取Canvas元素。因为Canvas元素就是HTML文档中的canvas标签所以我们可以通过DOM API获取它代码如下
```
const canvas = document.querySelector('canvas');
```
获取了canvas元素后我们就可以通过getContext方法拿到它的上下文对象。具体的操作就是我们调用canvas.getContext传入参数2d。
```
const context = canvas.getContext('2d');
```
有了2d上下文我们就可以开始绘制图形了。
**第二步,用 Canvas 上下文绘制图形。**
我们拿到的context对象上会有许多API它们大体上可以分为两类一类是设置状态的API可以设置或改变当前的绘图状态比如改变要绘制图形的颜色、线宽、坐标变换等等另一类是绘制指令API用来绘制不同形状的几何图形。
如何使用这些API呢我来举个例子假设我们要在画布的中心位置绘制一个100`*`100的红色正方形。那我们该怎么做呢
首先我们要通过计算得到Canvas画布的中心点。前面我们已经说过Canvas坐标系的左上角坐标是(0,0)右下角是Canvas的画布坐标canvas.width,canvas.height所以画布的中心点坐标是0.5 `*`canvas.width, 0.5 `*` canvas.height
如果我们要在中心点绘制一个100 `*` 100的正方形那对应的 Canvas指令是
```
context.rect(0.5 * canvas.width, 0.5 * canvas.height, 100, 100);
```
其中context.rect是绘制矩形的Canvas指令它的四个参数分别表示要绘制的矩形的x、y坐标和宽高。在这里我们要绘制的正方形宽高都是100所以后两个参数是100和100。
那在实际绘制之前我们还有一些工作要做。我要将正方形填充成红色这一步通过调用context.fillStyle指令就可以完成。然后我们要调用一个beginPath的指令告诉Canvas我们现在绘制的路径。接着才是调用 rect 指令完成绘图。最后,我们还要调用 fill 指令,将绘制的内容真正输出到画布中。这样我们就完整了绘制,绘制的效果和代码如下:
```
const rectSize = [100, 100];
context.fillStyle = 'red';
context.beginPath();
context.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize);
context.fill();
```
<img src="https://static001.geekbang.org/resource/image/1c/44/1c13a4yyd5f8284edb29038178eb9144.jpeg" alt="">
但是,看到上面这张图,我们发现了一个问题:正方形并没有居于画布的正中心。这是为什么呢?
你可以回想一下我们刚才的操作在绘制正方形的时候我们将rect指令的参数x、y设为画布宽高的一半。而rect指令的x、y的值表示的是我们要绘制出的矩形的左上角坐标而不是中心点坐标所以绘制出来的正方形自然就不在正中心了。
那我们该如何将正方形的中心点放在画布的中心呢?这就需要我们移动一下图形中心的坐标了。具体的实现方法有两种。
**第一种做法**是我们可以让rect指令的x、y参数等于画布宽高的一半分别减去矩形自身宽高的一半。这样我们就把正方形的中心点真正地移动到画布中心了。代码如下所示
```
context.rect(0.5 * (canvas.width - rectSize[0]), 0.5 * (canvas.height - rectSize[1]), ...rectSize);
```
**第二种做法**是我们可以先给画布设置一个平移变换Translate然后再进行绘制。代码如下所示
```
context.translate(-0.5 * rectSize[0], -0.5 * rectSize[1]);
```
在上面的代码中,我们给画布设置了一个平移,平移距离为矩形宽高的一半,这样它的中心点就是画布的中心了。
<img src="https://static001.geekbang.org/resource/image/3a/56/3ab2306311ba2382301ae9b654e62856.jpeg" alt="">
既然这两种方法都能将图形绘制到画布的中心,那我们该怎么选择呢?其实,我们可以从这两种方式各自的优缺点入手,下面我就具体来说一说。
第一种方式很简单,它直接改变了我们要绘制的图形顶点的坐标位置,但如果我们绘制的不是矩形,而是很多顶点的多边形,我们就需要在绘图前重新计算出每个顶点的位置,这会非常麻烦。
第二种方式是对Canvas画布的整体做一个平移操作这样我们只需要获取中心点与左上角的偏移然后对画布设置translate变换就可以了不需要再去改变图形的顶点位置。不过这样一来我们就改变了画布的状态。如果后续还有其他的图形需要绘制我们一定要记得把画布状态给恢复回来。好在这也不会影响到我们已经画好的图形。
那怎么把画布状态恢复回来呢?恢复画布状态的方式有两种,**第一种是反向平移。**反向平移的原理也很简单,你可以直接看下面的代码。
```
// 平移
context.translate(-0.5 * rectSize[0], -0.5 * rectSize[1]);
... 执行绘制
// 恢复
context.translate(0.5 * rectSize[0], 0.5 * rectSize[1]);
```
除了使用反向平移的恢复方式以外Canvas上下文还提供了**save和restore**方法可以暂存和恢复画布某个时刻的状态。其中save指令不仅可以保存当前的translate状态还可以保存其他的信息比如fillStyle等颜色信息。 而restore指令则可以将状态指令恢复成save指令前的设置。操作代码如下
```
context.save(); // 暂存状态
// 平移
context.translate(-0.5 * rectSize[0], -0.5 * rectSize[1]);
... 执行绘制
context.restore(); // 恢复状态
```
好了把一个简单矩形绘制到画布中心的完整方法我们已经说完了。那我们再来回顾一下利用Canvas绘制矩形的过程。我把这个过程总结为了5个步骤
1. 获取Canvas对象通过getContext(2d)得到2D上下文
1. 设置绘图状态比如填充颜色fillStyle平移变换translate等等
1. 调用 beginPath 指令开始绘制图形;
1. 调用绘图指令比如rect表示绘制矩形
1. 调用fill指令将绘制内容真正输出到画布上。
除此之外Canvas上下文还提供了更多丰富的API可以用来绘制直线、多边形、弧线、圆、椭圆、扇形和贝塞尔曲线等等这里我们不一一介绍了。在之后的课程中我们会详细讲解如何利用这些API来绘制复杂的几何图形。如果你还想了解更多关于Canvas的API相关的知识还可以去阅读[MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial)。
## 如何用Canvas 绘制层次关系图?
知道了Canvas的基本用法之后接下来我们就可以利用Canvas给一组城市数据绘制一个层次关系图了。也就是在一组给出的层次结构数据中体现出同属于一个省的城市。
在操作之前呢,我们先引入一个概念层次结构数据 Hierarchy Data它是可视化领域的专业术语用来表示能够体现层次结构的信息例如城市与省与国家。一般来说层次结构数据用层次关系图表来呈现。
其中城市数据是一组JSON格式的数据如下所示。
```
{
&quot;name&quot;:&quot;中国&quot;,
&quot;children&quot;:
[
{
&quot;name&quot;:&quot;浙江&quot; ,
&quot;children&quot;:
[
{&quot;name&quot;:&quot;杭州&quot; },
{&quot;name&quot;:&quot;宁波&quot; },
{&quot;name&quot;:&quot;温州&quot; },
{&quot;name&quot;:&quot;绍兴&quot; }
]
},
{
&quot;name&quot;:&quot;广西&quot; ,
&quot;children&quot;:
[
{&quot;name&quot;:&quot;桂林&quot;},
{&quot;name&quot;:&quot;南宁&quot;},
...
}
]
}
```
我们要绘制的层次关系图效果如下:
<img src="https://static001.geekbang.org/resource/image/07/bf/072dbcd9607dd7feafa47e34f97784bf.jpeg" alt="">
知道了要求之后我们应该怎么做呢首先我们要从数据源获取JSON数据。
```
const dataSource = 'https://s5.ssl.qhres.com/static/b0695e2dd30daa64.json';
(async function () {
const data = await (await fetch(dataSource)).json();
}());
```
这份JSON数据中只有“城市&gt;省份&gt;中国”这样的层级数据,我们要将它与绘图指令建立联系。建立联系指的是,我们要把数据的层级、位置和要绘制的圆的半径、位置一一对应起来。
换句话说我们要把数据转换成图形信息这个步骤需要数学计算。不过我们可以直接使用d3-hierarchy[d3-hierarchy](https://github.com/d3/d3-hierarchy)这个工具库转换数据。
```
(async function () {
const data = await (await fetch(dataSource)).json();
const regions = d3.hierarchy(data)
.sum(d =&gt; 1)
.sort((a, b) =&gt; b.value - a.value);
const pack = d3.pack()
.size([1600, 1600])
.padding(3);
const root = pack(regions);
}());
```
数学计算的内容我会在数据篇中详细来讲。这里我们就先不介绍d3的具体转换实现了你只需要知道我们可以用d3.hierarchy(data).sum(…).sort(…)将省份数据按照包含城市的数量,从多到少排序就可以了。
假设我们要将它们展现在一个画布宽高为1600 * 1600的Canvas中那我们可以通过d3.pack()将数据映射为一组1600宽高范围内的圆形。不过为了展示得美观一些在每个相邻的圆之间我们还保留3个像素的padding按照经验我们一般是保留3个像素padding的但这也要根据实际的设计需求来更改
这样,我们就获得了包含几何图形信息的数据对象。此时它的内部结构如下所示:
```
{
data: {name: '中国', children: [...]},
children: [
{
data: {name: '江苏', children: [...]},
value: 7,
r: 186.00172579386546,
x: 586.5048250548921,
y: 748.2441892254667,
}
...
],
value: 69,
x: 800,
y: 800,
r: 800,
}
```
我们需要的信息是数据中的x、y、r这些数值是前面调用d3.hierarchy帮我们算出来的。接下来我们只需要用Canvas将它们绘制出来就可以了。具体绘制过程比较简单**只需要遍历数据并且根据数据内容绘制圆弧**,我也把它总结成了两步。
第一步我们在当前数据节点绘制一个圆圆可以使用arc指令来绘制。arc方法的五个参数分别是圆心的x、y坐标、半径r、起始角度和结束角度前三个参数就是数据中的x、y和r。因为我们要绘制的是整圆所以后面的两个参数中起始角是0结束角是2π。
第二步绘制图成后如果这个数据节点有下一级数据我们遍历它的下一级数据然后递归地对这些数据调用绘图过程。如果没有下一级数据说明当前数据为城市数据那么我们就直接给出城市的名字这一步可以通过fillText指令来完成。具体的代码如下所示
```
const TAU = 2 * Math.PI;
function draw(ctx, node, {fillStyle = 'rgba(0, 0, 0, 0.2)', textColor = 'white'} = {}) {
const children = node.children;
const {x, y, r} = node;
ctx.fillStyle = fillStyle;
ctx.beginPath();
ctx.arc(x, y, r, 0, TAU);
ctx.fill();
if(children) {
for(let i = 0; i &lt; children.length; i++) {
draw(ctx, children[i]);
}
} else {
const name = node.data.name;
ctx.fillStyle = textColor;
ctx.font = '1.5rem Arial';
ctx.textAlign = 'center';
ctx.fillText(name, x, y);
}
}
draw(context, root);
```
这样我们就用Canvas绘图简单地实现了一个层次关系图它的代码不多也不复杂你可以很容易理解所以我就不做过多的解释啦。
## Canvas有哪些优缺点
通过上面的例子相信你已经熟悉了Canvas的基础用法。但是浏览器是一个复杂的图形环境要想灵活使用Canvas我们还需要从宏观层面来知道它能做什么不能做什么。
简单来说就是要了解Canvas 的优缺点。
首先Canvas是一个非常简单易用的图形系统。结合刚才的例子你也能感受到Canvas通过一组简单的绘图指令就能够方便快捷地绘制出各种复杂的几何图形。
另外Canvas渲染起来相当高效。即使是绘制大量轮廓非常复杂的几何图形Canvas也只需要调用一组简单的绘图指令就能高性能地完成渲染。这个呀其实和Canvas更偏向于渲染层能够提供底层的图形渲染API有关。那在实际实现可视化业务的时候Canvas出色的渲染能力正是它的优势所在。
不过Canvas也有缺点因为Canvas在HTML层面上是一个独立的画布元素所以所有的绘制内容都是在内部通过绘图指令来完成的绘制出的图形对于浏览器来说只是Canvas中的一个个像素点我们很难直接抽取其中的图形对象进行操作。
比如说在HTML或SVG中绘制一系列图形的时候我们可以一一获取这些图形的元素对象然后给它们绑定用户事件。但同样的操作在Canvas中没有可以实现的简单方法但是我们仍然可以和Canvas图形交互在后续课程中我们会有专门讨论。下一节课中我们会详细讲解SVG图形系统到时你就会更加明白它们的差异具体是什么了。
## 要点总结
好了Canvas的使用讲完了我们来总结一下你要掌握的重点内容。
首先我们讲了利用Canvas绘制几何图形这个过程很简单不过依然有3点需要我们注意
1. 在HTML中建立画布时我们要分别设置画布宽高和样式宽高
1. 在建立坐标系时我们要注意canvas的坐标系和笛卡尔坐标系在y轴上是相反的
1. 如果要把图形绘制在画布中心我们不能直接让x、y的坐标等于画布中心坐标而是要让图形中心和画布中心的位置重叠。这个操作我们可以通过计算顶点坐标或者 平移变换来实现。
接着我们讲了利用Canvas展示数据的层级关系。在这个过程中我们应当先处理数据将数据内容与绘图指令建立映射关系然后遍历数据通过映射关系将代表数据内容的参数传给绘图指令最后将图形绘制到Canvas上。
另外我们还讲了Canvas的优缺点。在实际实现可视化业务的时候Canvas的简单易操作和高效的渲染能力是它的优势但是它的缺点是不能方便地控制它内部的图形元素。
最后我还有一点想要补充一下。我们今天绘制的图形都是静态的如果要使用Canvas绘制动态的图形也很简单我们可以通过clearRect指令将之前的图形擦除再把新的图形绘制上去即可。在后续课程中我们有专门的章节来介绍动画。
## 小试牛刀
最后呢,我为你准备了两道课后题,试着动手操作一下吧!
1. 这节课我们介绍了用Canvas绘制矩形和圆弧实际上Canvas还有更多的绘图指令来绘制不同类型的几何图形。你可以试着修改一下前面显示正方形的例子在画布的中心位置显示一个三角形、椭圆或五角星。
1. Canvas的缺点是不能方便地控制它内部的图形元素但这不代表它完全不能控制。你可以尝试给我们前面绘制的层次关系图增加鼠标的交互让鼠标指针在移动到某个城市所属的圆的时候这个圆显示不同的颜色提示你可以获取鼠标坐标然后用这个坐标到圆心的距离来判断
欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课见!
## 源码
[用Canvas绘制层次关系图的完整代码.](https://github.com/akira-cn/graphics/tree/master/canvas/hierarchy)
## 推荐阅读
[1] [MDN官方文档.](https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial)

View File

@@ -0,0 +1,255 @@
<audio id="audio" title="03 | 声明式图形系统如何用SVG图形元素绘制可视化图表" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/42/a5/4256090856681648991eb448e236d6a5.mp3"></audio>
你好我是月影。今天我们来讲SVG。
SVG的全称是Scalable Vector Graphics可缩放矢量图它是浏览器支持的一种基于XML语法的图像格式。
对于前端工程师来说使用SVG的门槛很低。因为描述SVG的XML语言本身和HTML非常接近都是由标签+属性构成的而且浏览器的CSS、JavaScript都能够正常作用于SVG元素。这让我们在操作SVG时没什么特别大的难度。甚至我们可以认为**SVG就是HTML的增强版**。
对于可视化来说SVG是非常重要的图形系统。它既可以用JavaScript操作绘制各种几何图形还可以作为浏览器支持的一种图片格式来 独立使用img标签加载或者通过Canvas绘制。即使我们选择使用HTML和CSS、Canvas2D或者WebGL的方式来实现可视化但我们依然可以且很有可能会使用到SVG图像。所以关于SVG我们得好好学。
那这一节课我们就来聊聊SVG是怎么绘制可视化图表的以及它的局限性是什么。希望通过今天的讲解你能掌握SVG的基本用法和使用场景。
## 利用SVG绘制几何图形
在第1节课我们讲过SVG属于**声明式绘图系统**它的绘制方式和Canvas不同它不需要用JavaScript操作绘图指令只需要和HTML一样声明一些标签就可以实现绘图了。
那SVG究竟是如何绘图的呢我们先来看一个SVG声明的例子。
```
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot;&gt;
&lt;circle cx=&quot;100&quot; cy=&quot;50&quot; r=&quot;40&quot; stroke=&quot;black&quot;
stroke-width=&quot;2&quot; fill=&quot;orange&quot; /&gt;
&lt;/svg&gt;
```
在上面的代码中svg元素是SVG的根元素属性xmlns是xml的名字空间。那第一行代码就表示svg元素的xmlns属性值是"[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)"浏览器根据这个属性值就能够识别出这是一段SVG的内容了。
svg元素下的circle元素表示这是一个绘制在SVG图像中的圆形属性cx和cy是坐标表示圆心的位置在图像的x=100、y=50处。属性r表示半径r=40表示圆的半径为40。
以上就是这段代码中的主要属性。如果仔细观察你会发现我们并没有给100、50、40指定单位。这是为什么呢
因为SVG坐标系和Canvas坐标系完全一样都是以图像左上角为原点x轴向右y轴向下的左手坐标系。而且在默认情况下SVG坐标与浏览器像素对应所以100、50、40的单位就是px也就是像素不需要特别设置。
说到这你还记得吗在Canvas中为了让绘制出来的图形适配不同的显示设备我们要设置Canvas画布坐标。同理我们也可以通过给svg元素设置viewBox属性来改变SVG的坐标系。如果设置了viewBox属性那SVG内部的绘制就都是相对于SVG坐标系的了。
现在我们已经知道上面这段代码的含义了。那接下来我们把它写入HTML文档中就可以在浏览器中绘制出一个带黑框的橙色圆形了。
<img src="https://static001.geekbang.org/resource/image/c0/7f/c0617044406c562834c9e3db9d6d877f.jpg" alt="" title="黑色外框的圆形">
现在我们已经知道了SVG的基本用法了。总的来说它和HTML的用法基本一样你可以参考HTML的用法。那接下来我还是以上一节课实现的层次关系图为例来看看使用SVG该怎么实现。
## 利用SVG绘制层次关系图
我们先来回忆一下,上一节课我们要实现的层次关系图,是在一组给出的层次结构数据中,体现出同属于一个省的城市。数据源和前一节课相同,所以数据的获取部分并没有什么差别。这里我就不列出来了,我们直接来讲绘制的过程。
首先我们要将获取Canvas对象改成获取SVG对象方法是一样的还是通过选择器来实现。
```
const svgroot = document.querySelector('svg');
```
然后我们同样实现draw方法从root开始遍历数据对象。 不过在draw方法里我们不是像上一讲那样通过Canvas的2D上下文调用绘图指令来绘图而是通过创建SVG元素将元素添加到DOM文档里让图形显示出来。具体代码如下
```
function draw(parent, node, {fillStyle = 'rgba(0, 0, 0, 0.2)', textColor = 'white'} = {}) {
const {x, y, r} = node;
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', r);
circle.setAttribute('fill', fillStyle);
parent.appendChild(circle);
...
}
draw(svgroot, root);
```
从上面的代码中你可以看到,我们是使用**document.createElementNS方法**来创建SVG元素的。这里你要注意与使用document.createElement方法创建普通的HTML元素不同SVG元素要使用document.createElementNS方法来创建。
其中第一个参数是名字空间对应SVG名字空间http://www.w3.org/2000/svg。第二个参数是要创建的元素标签名因为要绘制圆型所以我们还是创建circle元素。然后我们将x、y、r分别赋给circle元素的cx、cy、r属性将fillStyle赋给circle元素的fill属性。最后我们将circle元素添加到它的parent元素上去。
接着我们遍历下一级数据。这次我们创建一个SVG的g元素递归地调用draw方法。具体代码如下
```
if(children) {
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
for(let i = 0; i &lt; children.length; i++) {
draw(group, children[i], {fillStyle, textColor});
}
parent.appendChild(group);
}
```
SVG的g元素表示一个分组我们可以用它来对SVG元素建立起层级结构。而且如果我们给g元素设置属性那么它的子元素会继承这些属性。
最后如果下一级没有数据了那我们还是需要给它添加文字。在SVG中添加文字只需要创建text元素然后给这个元素设置属性就可以了。操作非常简单你看我给出的代码就可以理解了。
```
else {
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('fill', textColor);
text.setAttribute('font-family', 'Arial');
text.setAttribute('font-size', '1.5rem');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('x', x);
text.setAttribute('y', y);
const name = node.data.name;
text.textContent = name;
parent.appendChild(text);
}
```
这样我们就实现了SVG版的层次关系图。你看它是不是看起来和前一节利用Canvas绘制的层次关系图没什么差别纸上得来终觉浅你可以自己动手实现一下这样理解得会更深刻。
<img src="https://static001.geekbang.org/resource/image/07/bf/072dbcd9607dd7feafa47e34f97784bf.jpeg" alt="" title="层次关系图">
## SVG和Canvas的不同点
那么问题就来了既然SVG和Canvas最终的实现效果没什么差别那在实际使用的时候我们该如何选择呢这就需要我们了解SVG和Canvas在使用上的不同点。知道了这些不同点我们就能在合适的场景下选择合适的图形系统了。
SVG和Canvas在使用上的不同主要可以分为两点分别是**写法上的不同**和**用户交互实现上**的不同。下面,我们一一来看。
### 1. 写法上的不同
第1讲我们说过SVG是以创建图形元素绘图的“声明式”绘图系统Canvas是执行绘图指令绘图的“指令式”绘图系统。那它们在写法上具体有哪些不同呢我们以层次关系图的绘制过程为例来对比一下。
在绘制层次关系图的过程中SVG首先通过创建标签来表示图形元素circle表示圆g表示分组text表示文字。接着SVG通过元素的setAttribute给图形元素赋属性值这个和操作HTML元素是一样的。
而Canvas先是通过上下文执行绘图指令来绘制图形画圆是调用context.arc指令然后再调用context.fill绘制画文字是调用context.fillText指令。另外Canvas还通过上下文设置状态属性context.fillStyle设置填充颜色conext.font设置元素的字体。我们设置的这些状态在绘图指令执行时才会生效。
从写法上来看因为SVG的声明式类似于HTML书写方式本身对前端工程师会更加友好。但是SVG图形需要由浏览器负责渲染和管理将元素节点维护在DOM树中。这样做的缺点是在一些动态的场景中也就是需要频繁地增加、删除图形元素的场景中SVG与一般的HTML元素一样会带来DOM操作的开销所以SVG的渲染性能相对比较低。
那除了写法不同以外SVG和Canvas还有其他区别吗当然是有的不过我要先卖一个关子我们讲完一个例子再来说。
### 2. 用户交互实现上的不同
我们尝试给这个SVG版本的层次关系图添加一个功能也就是当鼠标移动到某个区域时这个区域会高亮并且显示出对应的省-市信息。
因为SVG的一个图形对应一个元素所以我们可以像处理DOM元素一样很容易地给SVG图形元素添加对应的鼠标事件。具体怎么做呢我们一起来看。
首先我们要给SVG的根元素添加mousemove事件添加代码的操作很简单你可以直接看代码。
```
let activeTarget = null;
svgroot.addEventListener('mousemove', (evt) =&gt; {
let target = evt.target;
if(target.nodeName === 'text') target = target.parentNode;
if(activeTarget !== target) {
if(activeTarget) activeTarget.setAttribute('fill', 'rgba(0, 0, 0, 0.2)');
}
target.setAttribute('fill', 'rgba(0, 128, 0, 0.1)');
activeTarget = target;
});
```
就像是我们熟悉的HTML用法一样我们通过事件冒泡可以处理每个圆上的鼠标事件。然后我们把当前鼠标所在的圆的颜色填充成rgba(0, 128, 0, 0.1)’,这个颜色是带透明的浅绿色。最终的效果就是当我们的鼠标移动到圆圈范围内的时候,当前鼠标所在的圆圈变为浅绿色。你也可以尝试设置其他的值,看看不同的实现效果。
接着,我们要实现显示对应的省-市信息。在这里我们需要修改一下draw方法。具体的修改过程可以分为两步。
第一步是把省、市信息通过扩展属性data-name设置到svg的circle元素上这样我们就可以在移动鼠标的时候通过读取鼠标所在元素的属性拿到我们想要展示的省、市信息了。具体代码如下
```
function draw(parent, node, {fillStyle = 'rgba(0, 0, 0, 0.2)', textColor = 'white'} = {}) {
...
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
...
circle.setAttribute('data-name', node.data.name);
...
if(children) {
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
...
group.setAttribute('data-name', node.data.name);
...
} else {
...
}
}
```
第二步我们要实现一个getTitle方法从当前鼠标事件的target往上找parent元素拿到“省-市”信息把它赋给titleEl元素。这个titleEl元素是我们添加到网页上的一个h1元素用来显示省、市信息。
```
const titleEl = document.getElementById('title');
function getTitle(target) {
const name = target.getAttribute('data-name');
if(target.parentNode &amp;&amp; target.parentNode.nodeName === 'g') {
const parentName = target.parentNode.getAttribute('data-name');
return `${parentName}-${name}`;
}
return name;
}
```
最后我们就可以在mousemove事件中更新titleEl的文本内容了。
```
svgroot.addEventListener('mousemove', (evt) =&gt; {
...
titleEl.textContent = getTitle(target);
...
});
```
这样,我们就实现了给层次关系图增加鼠标控制的功能,最终的效果如下图所示,完整的代码我放在[GitHub仓库](https://github.com/akira-cn/graphics/tree/master/svg)了,你可以自己去查看。
<img src="https://static001.geekbang.org/resource/image/9c/2f/9c807df0cb2b4afyye6be3345a9c1a2f.gif" alt="">
其实我们上面讲的鼠标控制功能就是一个简单的用户交互功能。总结来说利用SVG的一个图形对应一个svg元素的机制我们就可以像操作普通的HTML元素那样给svg元素添加事件实现用户交互了。所以SVG有一个非常大的优点那就是**可以让图形的用户交互非常简单**。
和SVG相比利用Canvas对图形元素进行用户交互就没有那么容易了。不过对于圆形的层次关系图来说在Canvas图形上定位鼠标处于哪个圆中并不难我们只需要计算一下鼠标到每个圆的圆心距离如果这个距离小于圆的半径我们就可以确定鼠标在某个圆内部了。这实际上就是上一节课我们留下的思考题相信现在你应该可以做出来了。
但是试想一下如果我们要绘制的图形不是圆、矩形这样的规则图形而是一个复杂得多的多边形我们又该怎样确定鼠标在哪个图形元素的内部呢这对于Canvas来说就是一个比较复杂的问题了。不过这也不是不能解决的在后续的课程中我们就会讨论如何用数学计算的办法来解决这个问题。
## 绘制大量几何图形时SVG的性能问题
虽然使用SVG绘图能够很方便地实现用户交互但是有得必有失SVG这个设计给用户交互带来便利性的同时也带来了局限性。为什么这么说呢因为它和DOM元素一样以节点的形式呈现在HTML文本内容中依靠浏览器的DOM树渲染。如果我们要绘制的图形非常复杂这些元素节点的数量就会非常多。而节点数量多就会大大增加DOM树渲染和重绘所需要的时间。
就比如说在绘制如上的层次关系图时我们只需要绘制数十个节点。但是如果是更复杂的应用比如我们要绘制数百上千甚至上万个节点这个时候DOM树渲染就会成为性能瓶颈。事实上在一般情况下当SVG节点超过一千个的时候你就能很明显感觉到性能问题了。
幸运的是对于SVG的性能问题我们也是有解决方案的。比如说我们可以使用虚拟DOM方案来尽可能地减少重绘这样就可以优化SVG的渲染。但是这些方案只能解决一部分问题当节点数太多时这些方案也无能为力。这个时候我们还是得依靠Canvas和WebGL来绘图才能彻底解决问题。
那在上万个节点的可视化应用场景中SVG就真的一无是处了吗当然不是。SVG除了嵌入HTML文档的用法还可以直接作为一种图像格式使用。所以即使是在用Canvas和WebGL渲染的应用场景中我们也依然可能会用到SVG将它作为一些局部的图形使用这也会给我们的应用实现带来方便。在后续的课程中我们会遇到这样的案例。
## 要点总结
这一节课我们学习了SVG的基本用法、优点和局限性。
我们知道SVG作为一种浏览器支持的图像格式既可以作为HTML内嵌元素使用也可以作为图像通过img元素加载或者绘制到Canvas内。
而用SVG绘制可视化图形与用Canvas绘制有明显区别SVG通过创建标签来表示图形元素然后将图形元素添加到DOM树中交给DOM完成渲染。
使用DOM树渲染可以让图形元素的用户交互实现起来非常简单因为我们可以直接对图形元素注册事件。但是这也带来问题如果图形复杂那么SVG的图形元素会非常多这会导致DOM树渲染成为性能瓶颈。
## 小试牛刀
<li>
DOM操作SVG元素和操作普通的HTML元素几乎没有差别所以CSS也同样可以作用于SVG元素。那你可以尝试使用CSS来设置这节课我们实现的层级关系图里circle的背景色和文字属性。接着你也可以进一步想一想这样做有什么好处
</li>
<li>
因为SVG可以作为一种图像格式使用所以我们可以将生成的SVG作为图像然后绘制到Canvas上。那如果我们先用SVG生成层级关系图再用Canvas来完成绘制的话和我们单独使用它们来绘图有什么不同为什么
</li>
欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课见!
## 源码
[用SVG绘制层次关系图和给层次关系图增加鼠标控制的完整代码.](https://github.com/akira-cn/graphics/tree/master/svg)

View File

@@ -0,0 +1,357 @@
<audio id="audio" title="04 | GPU与渲染管线如何用WebGL绘制最简单的几何图形" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ed/86/edff7bb4e68f0196ab771eyye70b9586.mp3"></audio>
你好我是月影。今天我们要讲WebGL。
WebGL是最后一个和可视化有关的图形系统也是最难学的一个。为啥说它难学呢我觉得这主要有两个原因。第一WebGL这种技术本身就是用来解决最复杂的视觉呈现的。比如说大批量绘制复杂图形和3D模型这类比较有难度的问题就适合用WebGL来解决。第二WebGL相对于其他图形系统来说是一个更“开放”的系统。
我说的“开放”是针对于底层机制而言的。因为不管是HTML/CSS、SVG还是Canvas都主要是使用其API来绘制图形的所以我们不必关心它们具体的底层机制。也就是说我们只要理解创建SVG元素的绘图声明学会执行Canvas对应的绘图指令能够将图形输出这就够了。但是要使用WebGL绘图我们必须要深入细节里。换句话说就是我们必须要和内存、GPU打交道真正控制图形输出的每一个细节。
所以想要学好WebGL我们必须先理解一些基本概念和原理。那今天这一节课我会从图形系统的绘图原理开始讲起主要来讲WebGL最基础的概念包括GPU、渲染管线、着色器。然后我会带你用WebGL绘制一个简单的几何图形。希望通过这个可视化的例子能够帮助你理解WebGL绘制图形的基本原理打好绘图的基础。
## 图形系统是如何绘图的?
首先,我们来说说计算机图形系统的主要组成部分,以及它们在绘图过程中的作用。知道了这些,我们就能很容易理解计算机图形系统绘图的基本原理了。
一个通用计算机图形系统主要包括6个部分分别是输入设备、中央处理单元、图形处理单元、存储器、帧缓存和输出设备。虽然我下面给出了绘图过程的示意图不过这些设备在可视化中的作用我要再跟你多啰嗦几句。
- **光栅**Raster几乎所有的现代图形系统都是基于光栅来绘制图形的光栅就是指构成图像的像素阵列。
- **像素**Pixel一个像素对应图像上的一个点它通常保存图像上的某个具体位置的颜色等信息。
- **帧缓存**Frame Buffer在绘图过程中像素信息被存放于帧缓存中帧缓存是一块内存地址。
- **CPU**Central Processing Unit中央处理单元负责逻辑计算。
- **GPU**Graphics Processing Unit图形处理单元负责图形计算。
<img src="https://static001.geekbang.org/resource/image/b5/56/b5e4f37e1c4fbyy6a2ea10624d143356.jpg" alt="">
知道了这些概念,我带你来看一个典型的绘图过程,帮你来明晰一下这些概念的实际用途。
首先数据经过CPU处理成为具有特定结构的几何信息。然后这些信息会被送到GPU中进行处理。在GPU中要经过两个步骤生成光栅信息。这些光栅信息会输出到帧缓存中最后渲染到屏幕上。
<img src="https://static001.geekbang.org/resource/image/9f/46/9f7d76cc9126036ef966dc236df01c46.jpeg" alt="" title="图形数据经过GPU处理最终输出到屏幕上">
这个绘图过程是现代计算机中任意一种图形系统处理图形的通用过程。它主要做了两件事一是对给定的数据结合绘图的场景要素例如相机、光源、遮挡物体等等进行计算最终将图形变为屏幕空间的2D坐标。二是为屏幕空间的每个像素点进行着色把最终完成的图形输出到显示设备上。这整个过程是一步一步进行的前一步的输出就是后一步的输入所以我们也把这个过程叫做**渲染管线**RenderPipelines
在这个过程中CPU与GPU是最核心的两个处理单元它们参与了计算的过程。CPU我相信你已经比较熟悉了但是GPU又是什么呢别着急听我慢慢和你讲。
## GPU是什么
CPU和GPU都属于处理单元但是结构不同。形象点来说CPU就像个大的工业管道等待处理的任务就像是依次通过这个管道的货物。一条CPU流水线串行处理这些任务的速度取决于CPU管道的处理能力。
实际上一个计算机系统会有很多条CPU流水线而且任何一个任务都可以随机地通过任意一个流水线这样计算机就能够并行处理多个任务了。这样的一条流水线就是我们常说的**线程**Thread
[<img src="https://static001.geekbang.org/resource/image/1e/80/1e6479ef37138f051b7a6e5de6977580.jpeg" alt="" title="CPU">](https://thebookofshaders.com/)
这样的结构用来处理大型任务是足够的,但是要处理图像应用就不太合适了。这是因为,处理图像应用,实际上就是在处理计算图片上的每一个像素点的颜色和其他信息。每处理一个像素点就相当于完成了一个简单的任务,而一个图片应用又是由成千上万个像素点组成的,所以,我们需要在同一时间处理成千上万个小任务。
要处理这么多的小任务比起使用若干个强大的CPU使用更小、更多的处理单元是一种更好的处理方式。而GPU就是这样的处理单元。
[<img src="https://static001.geekbang.org/resource/image/1a/e7/1ab1116e3742611f5cb26c942d67d5e7.jpeg" alt="" title="GPU">](https://thebookofshaders.com/)
GPU是由大量的小型处理单元构成的它可能远远没有CPU那么强大但胜在数量众多可以保证每个单元处理一个简单的任务。即使我们要处理一张800 * 600大小的图片GPU也可以保证这48万个像素点分别对应一个小单元这样我们就可以**同时**对每个像素点进行计算了。
那GPU究竟是怎么完成像素点计算的呢这就必须要和WebGL的绘图过程结合起来说了。
## 如何用WebGL绘制三角形
浏览器提供的WebGL API是OpenGL ES的JavaScript绑定版本它赋予了开发者操作GPU的能力。这一特点也让WebGL的绘图方式和其他图形系统的“开箱即用”直接调用绘图指令或者创建图形元素就可以完成绘图的绘图方式完全不同甚至要复杂得多。我们可以总结为以下5个步骤
1. 创建WebGL上下文
1. 创建WebGL程序WebGL Program
1. 将数据存入缓冲区
1. 将缓冲区数据读取到GPU
1. GPU执行WebGL程序输出结果
别看这些步骤看起来很简单但其中会涉及许多你没听过的新概念、方法以及各种参数。不过这也不用担心我们今天的重点还是放在理解WebGL的基本用法和绘制原理上对于新的方法具体怎么用参数如何设置这些我们都会在后面的课程中详细来讲。
接下来,我们就用一个绘制三角形的例子,来讲一下这些步骤的具体操作过程。
### 步骤一创建WebGL上下文
创建WebGL上下文这一步和Canvas2D的使用几乎一样我们只要调用canvas元素的getContext即可区别是将参数从2d换成webgl
```
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
```
不过有了WebGL上下文对象之后我们并不能像使用Canvas2D的上下文那样调用几个绘图指令就把图形画出来还需要做很多工作。别着急让我们一步一步来。
### 步骤二创建WebGL程序
接下来我们要创建一个WebGL程序。你可能会觉得奇怪我们不是正在写一个绘制三角形的程序吗为什么这里又要创建一个WebGL程序呢实际上这里的WebGL程序是一个WebGLProgram对象它是给GPU最终运行着色器的程序而不是我们正在写的三角形的JavaScript程序。好了解决了这个疑问我们就正式开始创建一个WebGL程序吧
首先要创建这个WebGL程序我们需要编写两个**着色器**Shader。着色器是用GLSL这种编程语言编写的代码片段这里我们先不用过多纠结于GLSL语言在后续的课程中我们会详细讲解。那在这里我们只需要理解绘制三角形的这两个着色器的作用就可以了。
```
const vertex = `
attribute vec2 position;
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(position, 1.0, 1.0);
}
`;
const fragment = `
precision mediump float;
void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
```
那我们为什么要创建两个着色器呢?这就需要我们先来理解**顶点和图元**这两个基本概念了。在绘图的时候WebGL是以顶点和图元来描述图形几何信息的。顶点就是几何图形的顶点比如三角形有三个顶点四边形有四个顶点。图元是WebGL可直接处理的图形单元由WebGL的绘图模式决定有点、线、三角形等等。
所以顶点和图元是绘图过程中必不可少的。因此WebGL绘制一个图形的过程一般需要用到两段着色器一段叫**顶点着色器**Vertex Shader负责处理图形的顶点信息另一段叫**片元着色器**Fragment Shader负责处理图形的像素信息。
更具体点来说,我们可以把**顶点着色器理解为处理顶点的GPU程序代码。它可以改变顶点的信息**(如顶点的坐标、法线方向、材质等等),从而改变我们绘制出来的图形的形状或者大小等等。
顶点处理完成之后WebGL就会根据顶点和绘图模式指定的图元计算出需要着色的像素点然后对它们执行片元着色器程序。简单来说就是对指定图元中的像素点着色。
WebGL从顶点着色器和图元提取像素点给片元着色器执行代码的过程就是我们前面说的生成光栅信息的过程我们也叫它光栅化过程。所以**片元着色器的作用,就是处理光栅化后的像素信息。**
这么说可能比较抽象,我 来举个例子。我们可以将图元设为线段,那么片元着色器就会处理顶点之间的线段上的像素点信息,这样画出来的图形就是空心的。而如果我们把图元设为三角形,那么片元着色器就会处理三角形内部的所有像素点,这样画出来的图形就是实心的。
<img src="https://static001.geekbang.org/resource/image/6c/6e/6c4390eb21e653274db092a9ba71946e.jpg" alt="">
这里你要注意一点因为图元是WebGL可以直接处理的图形单元所以其他非图元的图形最终必须要转换为图元才可以被WebGL处理。举个例子如果我们要绘制实心的四边形我们就需要将四边形拆分成两个三角形再交给WebGL分别绘制出来。
好了,那让我们回到片元着色器对像素点着色的过程。你还要注意,这个过程是并行的。也就是说,**无论有多少个像素点,片元着色器都可以同时处理。**这也是片元着色器一大特点。
以上就是片元着色器的作用和使用特点了关于顶点着色器的作用我们一会儿再说。说了这么多你可别忘了创建着色器的目的是为了创建WebGL程序那我们应该如何用顶点着色器和片元着色器代码来创建WebGL程序呢
首先因为在JavaScript中顶点着色器和片元着色器只是一段代码片段所以我们要将它们分别创建成shader对象。代码如下所示
```
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(fragmentShader);
```
接着我们创建WebGLProgram对象并将这两个shader关联到这个WebGL程序上。WebGLProgram对象的创建过程主要是添加vertexShader和fragmentShader然后将这个WebGLProgram对象链接到WebGL上下文对象上。代码如下
```
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
```
最后我们要通过useProgram选择启用这个WebGLProgram对象。这样当我们绘制图形时GPU就会执行我们通过WebGLProgram设定的 两个shader程序了。
```
gl.useProgram(program);
```
好了现在我们已经创建并完成WebGL程序的配置。接下来 我们只要将三角形的数据存入缓冲区也就能将这些数据送入GPU了。那实现这一步之前呢我们先来认识一下WebGL的坐标系。
### 步骤三:将数据存入缓冲区
我们要知道WebGL的坐标系是一个三维空间坐标系坐标原点是0,0,0。其中x轴朝右y轴朝上z轴朝外。这是一个右手坐标系。
<img src="https://static001.geekbang.org/resource/image/yy/b1/yy3e873beb7743096e3cc7b641e718b1.jpeg" alt="">
假设,我们要在这个坐标系上显示一个顶点坐标分别是(-1, -11, -10, 1的三角形如下图所示。因为这个三角形是二维的所以我们可以直接忽略z轴。下面我们来一起绘图。
<img src="https://static001.geekbang.org/resource/image/83/c3/8311b485131497ce59cd1600b9a7f7c3.jpeg" alt="">
**首先,我们要定义这个三角形的三个顶点**。WebGL使用的数据需要用类型数组定义默认格式是Float32Array。Float32Array是JavaScript的一种类型化数组TypedArrayJavaScript通常用类型化数组来处理二进制缓冲区。
因为平时我们在Web前端开发中使用到类型化数组的机会并不多你可能还不大熟悉不过没关系类型化数组的使用并不复杂定义三角形顶点的过程你直接看我下面给出的代码就能理解。不过如果你之前完全没有接触过它我还是建议你阅读[MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray),去详细了解一下类型化数组的使用方法。
```
const points = new Float32Array([
-1, -1,
0, 1,
1, -1,
]);
```
**接着我们要将定义好的数据写入WebGL的缓冲区**。这个过程我们可以简单总结为三步分别是创建一个缓存对象将它绑定为当前操作对象再把当前的数据写入缓存对象。这三个步骤主要是利用createBuffer、bindBuffer、bufferData方法来实现的过程很简单你可以看一下我下面给出的实现代码。
```
const bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
```
### 步骤四将缓冲区数据读取到GPU
现在我们已经把数据写入缓存了但是我们的shader现在还不能读取这个数据还需要把数据绑定给顶点着色器中的position变量。
还记得我们的顶点着色器是什么样的吗?它是按如下的形式定义的:
```
attribute vec2 position;
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(position, 1.0, 1.0);
}
```
在GLSL中attribute表示声明变量vec2是变量的类型它表示一个二维向量position是变量名。接下来我们将buffer的数据绑定给顶点着色器的position变量。
```
const vPosition = gl.getAttribLocation(program, 'position');获取顶点着色器中的position变量的地址
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);给变量设置长度和类型
gl.enableVertexAttribArray(vPosition);激活这个变量
```
经过这样的处理在顶点着色器中我们定义的points类型数组中对应的值就能通过变量position读到了。
### 步骤五:执行着色器程序完成绘制
现在我们把数据传入缓冲区以后GPU也可以读取绑定的数据到着色器变量了。接下来我们只需要调用绘图指令就可以执行着色器程序来完成绘制了。
我们先调用gl.clear将当前画布的内容清除然后调用gl.drawArrays传入绘制模式。这里我们选择gl.TRIANGLES表示以三角形为图元绘制再传入绘制的顶点偏移量和顶点数量WebGL就会将对应的buffer数组传给顶点着色器并且开始绘制。代码如下
```
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);
```
这样我们就在Canvas画布上画出了一个红色三角形。
<img src="https://static001.geekbang.org/resource/image/cc/61/ccdd298c45f80a9a00d23082cf637d61.jpeg" alt="">
为什么是红色三角形呢?因为我们在片元着色器中定义了像素点的颜色,代码如下:
```
precision mediump float;
void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
```
在**片元着色器**里我们可以通过设置gl_FragColor的值来定义和改变图形的颜色。gl_FragColor是WebGL片元着色器的内置变量表示当前像素点颜色它是一个用RGBA色值表示的四维向量数据。在上面的代码中因为我们写入vec4(1.0, 0.0, 0.0, 1.0)对应的是红色所以三角形是红色的。如果我们把这个值改成vec4(0.0, 0.0, 1.0, 1.0),那三角形就是蓝色。
我为什么会强调颜色这个事儿呢你会发现刚才我们只更改了一个值就把整个图片的所有像素颜色都改变了。所以我们必须要认识到一点WebGL可以并行地对整个三角形的所有像素点同时运行片元着色器。并行处理是WebGL程序非常重要的概念所以我就多强调一下。
我们要记住不论这个三角形是大还是小有几十个像素点还是上百万个像素点GPU都是**同时处理**每个像素点的。也就是说图形中有多少个像素点着色器程序在GPU中就会被同时执行多少次。
到这里WebGL绘制三角形的过程我们就讲完了。借助这个过程我们加深了对顶点着色器和片元着色器在使用上的理解。不过因为后面我们会更多地讲解片元着色器的绘图方法那今天我们正好可以借着这个机会多讲讲顶点着色器的应用我希望你也能掌握好它。
## 顶点着色器的作用
顶点着色器大体上可以总结为两个作用一是通过gl_Position设置顶点二是通过定义varying变量向片元着色器传递数据。这么说还是有点抽象我们还是通过三角形的例子来具体理解一下。
### 1. 通过gl_Position设置顶点
假如我想把三角形的周长缩小为原始大小的一半有两种处理方式法一种是修改points数组的值另一种做法是直接对顶点着色器数据进行处理。第一种做法很简单我就不讲了如果不懂你可以在留言区提问。我们来详细说说第二种做法。
我们不需要修改points数据只需要在顶点着色器中将 gl_Position = vec4(position, 1.0, 1.0);修改为 gl_Position = vec4(position * 0.5, 1.0, 1.0);,代码如下所示。
```
attribute vec2 position;
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(position * 0.5, 1.0, 1.0);
}
```
这样三角形的周长就缩小为原来的一半了。在这个过程中我们不需要遍历三角形的每一个顶点只需要是利用GPU的并行特性在顶点着色器中同时计算所有的顶点就可以了。在后续课程中我们还会遇到更加复杂的例子但在那之前你一定要理解并牢记WebGL可以**并行计算**这一特点。
### 2. 向片元着色器传递数据
除了计算顶点之外顶点着色器还可以将数据通过varying变量传给片元着色器。然后这些值会根据片元着色器的像素坐标与顶点像素坐标的相对位置做**线性插值**。这是什么意思呢?其实这很难用文字描述,我们还是来看一段代码:
```
attribute vec2 position;
varying vec3 color;
void main() {
gl_PointSize = 1.0;
color = vec3(0.5 + position * 0.5, 0.0);
gl_Position = vec4(position * 0.5, 1.0, 1.0);
}
```
在这段代码中我们修改了顶点着色器定义了一个color变量它是一个三维的向量。我们通过数学技巧将顶点的值映射为一个RGB颜色值关于顶点映射RGB颜色值的方法在后续的课程中会有详细介绍映射公式是 vec3(0.5 + position * 0.5, 0.0)。
这样一来,顶点[-1,-1]被映射为[0,0,0]也就是黑色,顶点[0,1]被映射为[0.5, 1, 0]也就是浅绿色,顶点[1,-1]被映射为[1,0,0]也就是红色。这样一来,三个顶点就会有三个不同的颜色值。
然后我们将color通过varying变量传给片元着色器。片元着色器中的代码如下
```
precision mediump float;
varying vec3 color;
void main()
{
gl_FragColor = vec4(color, 1.0);
}
```
我们将gl_FragColor的rgb值设为变量color的值这样我们就能得到下面这个三角形
<img src="https://static001.geekbang.org/resource/image/5c/21/5c4c718eca069be33d8a1d5d1eb77821.jpeg" alt="">
我们可以看到这个三角形是一个颜色均匀线性渐变的三角形它的三个顶点的色值就是我们通过顶点着色器来设置的。而且你会发现中间像素点的颜色是均匀过渡的。这就是因为WebGL在执行片元着色器程序的时候顶点着色器传给片元着色器的变量会根据片元着色器的像素坐标对变量进行线性插值。利用线性插值可以让像素点的颜色均匀渐变这一特点我们就能绘制出颜色更丰富的图形了。
好了到这里我们就在Canvas画布上用WebGL绘制出了一个三角形。绘制三角形的过程就像我们初学编程时去写出一个Hello World程序一样按道理来说应该非常简单才对。但事实上用WebGL完成这个程序我们一共用了好几十行代码。而如果我们用Canvas2D或者SVG实现类似的功能只需要几行代码就可以了。
那我们为什么非要这么做呢而且我们费了很大的劲就只绘制出了一个最简单的三角形这似乎离我们用WebGL实现复杂的可视化效果还非常遥远。我想告诉你的是别失落想要利用WebGL绘制更有趣、更复杂的图形我们就必须要学会绘制三角形这个图元。还记得我们前面说过的要在WebGL中绘制非图元的其他图形时我们必须要把它们划分成三角形才行。学习了后面的课程之后你就会对这一点有更深刻的理解了。
而且用WebGL可以实现的视觉效果远远超越其他三个图形系统。如果用驾驶技术来比喻的话使用SVG和Canvas2D时就像我们在开一辆自动挡的汽车那么使用WebGL的时候就像是在开战斗机所以千万别着急随着对WebGL的不断深入理解我们就能用它来实现更多有趣的实例了。
## 要点总结
在这一节课我们讲了WebGL的绘图过程以及顶点着色器和片元着色器的作用。
WebGL图形系统与用其他图形系统不同它的API非常底层使用起来比较复杂。想要学好WebGL我们必须要从基础概念和原理学起。
一般来说在WebGL中要完成图形的绘制需要创建WebGL程序然后将图形的几何数据存入数据缓冲区在绘制过程中让WebGL从缓冲区读取数据并且执行着色器程序。
WebGL的着色器程序有两个。一个是顶点着色器负责处理图形的顶点数据。另一个是片元着色器负责处理光栅化后的像素信息。此外我们还要牢记WebGL程序有一个非常重要的特点就是能够并行处理无论图形中有多少个像素点都可以通过着色器程序在GPU中被同时执行。
WebGL完整的绘图过程实在比较复杂为了帮助你理解我总结一个流程图供你参考。
[<img src="https://static001.geekbang.org/resource/image/d3/30/d31e6c50b55872f81aa70625538fb930.jpg" alt="" title="WebGL绘图流程">](https://juejin.im/post/5e7a042e6fb9a07cb96b1627)
那到这里可视化的四个图形系统我们就介绍完了。但是好戏才刚刚开始哦在后续的文章中我们会围绕着这四个图形系统尤其是Canvas2D和WebGL逐渐深入来实现更多有趣的图形。
## 小试牛刀
<li>
WebGL通过顶点和图元来绘制图形我们在上面的例子中调用gl.TRIANGLES 绘制出了实心的三角形。如果要绘制空心三角形,我们又应该怎么做呢?有哪些图元类型可以帮助我们完成这个绘制?
</li>
<li>
三角形是最简单的几何图形如果我们要绘制其他的几何图形我们可以通过用多个三角形拼接来实现。试着用WebGL绘制正四边形、正五边形和正六角星吧
</li>
欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这一节课分享给你的朋友,我们下节课再见!
## 源码
[WebGL绘制三角形示例代码.](https://github.com/akira-cn/graphics/tree/master/webgl)
## 推荐阅读
[1] [类型化数组 MDN 文档.](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)<br>
[2] [WebGL 的 MDN 文档.](https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API)