mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-10-14 05:53:44 +08:00
mod
This commit is contained in:
205
极客时间专栏/跟月影学可视化/图形基础篇/01 | 浏览器中实现可视化的四种方式.md
Normal file
205
极客时间专栏/跟月影学可视化/图形基础篇/01 | 浏览器中实现可视化的四种方式.md
Normal 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和CSS,Canvas2D和WebGL更适合去做可视化这一领域的绘图工作。它们的绘图API能够直接操作绘图上下文,一般不涉及引擎的其他部分,在重绘图像时,也不会发生重新解析文档和构建结构的过程,开销要小很多。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d4/9d/d49d2fb673a7fb9f8de329c12fab009d.jpg" alt="" title="图形系统与浏览器渲染引擎工作对比">
|
||||
|
||||
## 方式二:SVG
|
||||
|
||||
在介绍Canvas2D和WebGL之前,我们先来说一说SVG。现代浏览器支持SVG(Scalable Vector Graphics,可缩放矢量图),SVG是一种基于 XML 语法的图像格式,可以用图片(img元素)的src属性加载。而且,浏览器更强大的是,它还可以内嵌SVG标签,并且像操作普通的HTML元素一样,利用DOM API操作SVG元素。甚至,CSS也可以作用于内嵌的SVG元素。
|
||||
|
||||
比如,上面的柱状图,如果用SVG实现的话,我们可以用如下所示的代码来实现:
|
||||
|
||||
```
|
||||
<!--
|
||||
dataset = {
|
||||
total: [25, 26, 40, 45, 68],
|
||||
current: [15, 11, 17, 25, 37],
|
||||
}
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="120px" height="240px" viewBox="0 0 60 100">
|
||||
<g transform="translate(0, 100) scale(1, -1)">
|
||||
<g>
|
||||
<rect x="1" y="0" width="10" height="25" fill="#37c"/>
|
||||
<rect x="13" y="0" width="10" height="26" fill="#37c"/>
|
||||
<rect x="25" y="0" width="10" height="40" fill="#37c"/>
|
||||
<rect x="37" y="0" width="10" height="45" fill="#37c"/>
|
||||
<rect x="49" y="0" width="10" height="68" fill="#37c"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="1" y="0" width="10" height="15" fill="#3c7"/>
|
||||
<rect x="13" y="0" width="10" height="11" fill="#3c7"/>
|
||||
<rect x="25" y="0" width="10" height="17" fill="#3c7"/>
|
||||
<rect x="37" y="0" width="10" height="25" fill="#3c7"/>
|
||||
<rect x="49" y="0" width="10" height="37" fill="#3c7"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
```
|
||||
|
||||
从上面的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来绘制文中的柱状图。
|
||||
|
||||
欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课见!
|
364
极客时间专栏/跟月影学可视化/图形基础篇/02 | 指令式绘图系统:如何用Canvas绘制层次关系图?.md
Normal file
364
极客时间专栏/跟月影学可视化/图形基础篇/02 | 指令式绘图系统:如何用Canvas绘制层次关系图?.md
Normal 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元素。
|
||||
|
||||
```
|
||||
<body>
|
||||
<canvas width="512" height="512"></canvas>
|
||||
</body>
|
||||
|
||||
```
|
||||
|
||||
这里有一点需要特别注意,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,0)到(512,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格式的数据,如下所示。
|
||||
|
||||
```
|
||||
{
|
||||
"name":"中国",
|
||||
"children":
|
||||
[
|
||||
{
|
||||
"name":"浙江" ,
|
||||
"children":
|
||||
[
|
||||
{"name":"杭州" },
|
||||
{"name":"宁波" },
|
||||
{"name":"温州" },
|
||||
{"name":"绍兴" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"广西" ,
|
||||
"children":
|
||||
[
|
||||
{"name":"桂林"},
|
||||
{"name":"南宁"},
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们要绘制的层次关系图效果如下:
|
||||
|
||||
<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数据中只有“城市>省份>中国”这样的层级数据,我们要将它与绘图指令建立联系。建立联系指的是,我们要把数据的层级、位置和要绘制的圆的半径、位置一一对应起来。
|
||||
|
||||
换句话说,我们要把数据转换成图形信息,这个步骤需要数学计算。不过,我们可以直接使用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 => 1)
|
||||
.sort((a, b) => 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 < 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)
|
255
极客时间专栏/跟月影学可视化/图形基础篇/03 | 声明式图形系统:如何用SVG图形元素绘制可视化图表?.md
Normal file
255
极客时间专栏/跟月影学可视化/图形基础篇/03 | 声明式图形系统:如何用SVG图形元素绘制可视化图表?.md
Normal 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声明的例子。
|
||||
|
||||
```
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<circle cx="100" cy="50" r="40" stroke="black"
|
||||
stroke-width="2" fill="orange" />
|
||||
</svg>
|
||||
|
||||
```
|
||||
|
||||
在上面的代码中,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 < 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) => {
|
||||
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 && target.parentNode.nodeName === 'g') {
|
||||
const parentName = target.parentNode.getAttribute('data-name');
|
||||
return `${parentName}-${name}`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
最后,我们就可以在mousemove事件中更新titleEl的文本内容了。
|
||||
|
||||
```
|
||||
|
||||
svgroot.addEventListener('mousemove', (evt) => {
|
||||
...
|
||||
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)
|
357
极客时间专栏/跟月影学可视化/图形基础篇/04 | GPU与渲染管线:如何用WebGL绘制最简单的几何图形?.md
Normal file
357
极客时间专栏/跟月影学可视化/图形基础篇/04 | GPU与渲染管线:如何用WebGL绘制最简单的几何图形?.md
Normal 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, -1)、(1, -1)、(0, 1)的三角形,如下图所示。因为这个三角形是二维的,所以我们可以直接忽略z轴。下面,我们来一起绘图。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/83/c3/8311b485131497ce59cd1600b9a7f7c3.jpeg" alt="">
|
||||
|
||||
**首先,我们要定义这个三角形的三个顶点**。WebGL使用的数据需要用类型数组定义,默认格式是Float32Array。Float32Array是JavaScript的一种类型化数组(TypedArray),JavaScript通常用类型化数组来处理二进制缓冲区。
|
||||
|
||||
因为平时我们在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)
|
Reference in New Issue
Block a user