为什么要学习可视化

# 为什么要学习可视化

  • 现在很多 C 端或者 B 端的互联网产品都离不开可视化。

  • 可视化能实现很多传统 Web 网页无法实现的效果。

# 什么是数据可视化

数据可视化是将数据组织成易于为人所理解和认知的结构,然后用图形的方式形象地呈现出来的理论、方法和技术。

实现可视化有两个关键要素,一个是数据,另一个是图形。如果要考虑在计算机上呈现,那还要加上交互

# Web 前端与可视化的区别

首先是技术栈的不同。Web 开发主要会用到 HTML 和 CSS,而可视化则较少涉及 HTML 和 CSS,它更多地要同浏览器的 Canvas、SVG、WebGL 等其他图形 API 打交道。

其次,Web 开发着重于处理普通的文本和多媒体信息,渲染普通的、易于阅读的文本和多媒体内容,而可视化开发则着重于处理结构化数据,有时需要深入渲染引擎层,从而控制细节,让浏览器渲染出各种相对复杂的图表和图形元素。

简而言之,Web 开发的前端主要还是关注内容和样式,图形的渲染和绘制是由浏览器底层来完成的,而可视化前端则可能要深入底层渲染层,去真正地控制图形的绘制和细节的呈现。

# 浏览器中实现可视化的四种方式

# HTML + CSS

HTML + CSS 的优点是方便,不需要第三方依赖,甚至不需要 JavaScript 代码。如果我们要绘制少量常见的图表,可以直接采用 HTML 和 CSS。它的缺点是 CSS 属性不能直观体现数据,绘制起来也相对麻烦,图形复杂会导致 HTML 元素多,而消耗性能。

CSS 实现柱状图的原理就是使用网格布局加上线性渐变;实现饼图的原理就是使用圆锥渐变;实现折线图的原理是可以用高度很小的 Div 元素来模拟线段,然后用 transform 改变角度和位置,这样就能拼成折线图了,另外,如果使用 clip-path (opens new window) 这样的高级属性,还能实现更复杂的图表,比如,用不同的颜色表示两个不同折线的面积。

HTML + CSS 的方式绘制大量图形时之所以会很消耗性能,是因为 HTML 和 CSS 作为浏览器渲染引擎的一部分,为了完成页面渲染的工作,除了绘制图形外,还要做很多额外的工作。比如说,浏览器的渲染引擎在工作时,要先解析 HTML、SVG、CSS,构建 DOM 树、RenderObject 树和 RenderLayer 树,然后用 HTML(或 SVG)绘图。当图形发生变化时,很可能要重新执行全部的工作,这样的性能开销是非常大的。

图形系统与浏览器渲染引擎工作对比如下图:

# SVG

SVG(Scalable Vector Graphics,可缩放矢量图),是对 HTML/CSS 的增强,弥补了 HTML 绘制不规则图形的能力。它通过属性设置图形,可以直观地体现数据,使用起来非常方便。但是 SVG 也有和 HTML/CSS 同样的问题,图形复杂时需要的 SVG 元素太多,也非常消耗性能。

# Canvas2D

Canvas2D 是浏览器提供的简便快捷的指令式图形系统(而 HTML + CSS 和 SVG 是声明式图形系统),它通过一些简单的指令就能快速绘制出复杂的图形。由于它直接操作绘图上下文,因此没有 HTML/CSS 和 SVG 绘图因为元素多导致消耗性能的问题,性能要比前两者快得多。但是如果要绘制的图形太多,或者处理大量的像素计算时,Canvas2D 依然会遇到性能瓶颈。

# WebGL

WebGL 是浏览器提供的功能强大的绘图系统,它使用比较复杂,但是功能强大,能够充分利用 GPU 并行计算的能力,来快速、精准地操作图像的像素,在同一时间完成数十万或数百万次计算。另外,它还内置了对 3D 物体的投影、深度检测等处理,这让它更适合绘制 3D 场景。

# 可视化技术方案选型图

# 指令式绘图系统:Canvas

# Canvas 元素和 2D 上下文

Canvas (opens new window) 元素上的 width 和 height 属性不等同于 Canvas 元素的 CSS 样式的属性。这是因为,CSS 属性中的宽高影响 Canvas 在页面上呈现的大小,而 HTML 属性中的宽高则决定了 Canvas 的坐标系。为了区分它们,将 Canvas 的 HTML 属性宽高为画布宽高,CSS 样式宽高为样式宽高

因为画布宽高决定了可视区域的坐标范围,所以 Canvas 将画布宽高和样式宽高分开的做法,能更方便地适配不同的显示设备

比如,我们要在画布宽高为 500 _ 500 的 Canvas 画布上,绘制一个居中显示的 100 _ 100 宽高的正方形。我们只要将它的坐标设置在 x = 200,y = 200 处即可。这样,不论这个 Canvas 以多大的尺寸显示在各种设备上,我们的代码都不需要修改。否则,如果 Canvas 的坐标范围(画布宽高)跟着样式宽高变化,那么当屏幕尺寸改变的时候,我们就要重新计算需要绘制的图形的所有坐标,这对于我们来说将会是一场 “灾难”。

# Canvas 的坐标系

Canvas 的坐标系和浏览器窗口的坐标系类似,它们都默认左上角为坐标原点,x 轴水平向右,y 轴垂直向下。

# 利用 Canvas 绘制几何图形

使用 Canvas 绘制图形的过程可以总结为以下 5 个步骤:

  1. 获取 Canvas 对象,通过 getContext('2d') 得到 2D 上下文;

  2. 设置绘图状态,比如填充颜色 fillStyle,平移变换 translate 等等;

  3. 调用 beginPath 指令开始绘制图形;

  4. 调用绘图指令,比如 rect,表示绘制矩形;

  5. 调用 fill 指令,将绘制内容真正输出到画布上。

假设要在画布中心点绘制一个 100 * 100 的正方形:

const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const rectSize = [100, 100];

context.fillStyle = "red";
context.beginPath();
context.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize); // 四个参数分别表示要绘制的矩形的 x、y 坐标和宽高
context.fill();
1
2
3
4
5
6
7
8

但是,执行以上的代码后会发现,绘制出来的正方形并没有居于画布的正中心。这是因为在绘制正方形的时候,我们将 rect 指令的参数 x、y 设为画布宽高的一半。而 rect 指令的 x、y 的值表示的是,我们要绘制出的矩形的左上角坐标而不是中心点坐标,所以绘制出来的正方形自然就不在正中心了。

那么如何将正方形的中心点放在画布的中心呢?有 2 种方法:

  • 第一种做法是,可以让 rect 指令的 x、y 参数,等于画布宽高的一半分别减去矩形自身宽高的一半。
context.rect(0.5 * (canvas.width - rectSize[0]), 0.5 * (canvas.height - rectSize[1]);
1
  • 第二种做法是,可以先给画布设置一个平移变换(Translate),然后再进行绘制。
context.translate(-0.5 * rectSize[0], -0.5 * rectSize[1]);
1

这两种方法的优缺点如下:

第一种方式很简单,它直接改变了我们要绘制的图形顶点的坐标位置,但如果我们绘制的不是矩形,而是很多顶点的多边形,我们就需要在绘图前重新计算出每个顶点的位置,这会非常麻烦

第二种方式是对 Canvas 画布的整体做一个平移操作,这样我们只需要获取中心点与左上角的偏移,然后对画布设置 translate 变换就可以了,不需要再去改变图形的顶点位置。不过,这样一来我们就改变了画布的状态。如果后续还有其他的图形需要绘制,我们一定要记得把画布状态给恢复回来。好在,这也不会影响到我们已经画好的图形。

如何将画布状态恢复回来呢?也有 2 种方法:

  • 第一种是反向平移
// 平移
context.translate(-0.5 * rectSize[0], -0.5 * rectSize[1]);

// ... 执行绘制

// 恢复
context.translate(0.5 * rectSize[0], 0.5 * rectSize[1]);
1
2
3
4
5
6
7
  • 第二种是 Canvas 上下文提供了 save 和 restore 方法,可以暂存和恢复画布某个时刻的状态。其中,save 指令不仅可以保存当前的 translate 状态,还可以保存其他的信息,比如,fillStyle 等颜色信息。 而 restore 指令则可以将状态指令恢复成 save 指令前的设置。
context.save(); // 暂存状态

// 平移
context.translate(-0.5 * rectSize[0], -0.5 * rectSize[1]);

// ... 执行绘制

context.restore(); // 恢复状态
1
2
3
4
5
6
7
8

# 利用 Canvas 绘制层次关系图

在学习使用 Canvas 绘制层次关系图之前,需要先了解概念层次结构数据(Hierarchy Data),它是可视化领域的专业术语,用来表示能够体现层次结构的信息,例如城市与省与国家。一般来说,层次结构数据用层次关系图表来呈现

首先,我们要从数据源获取 JSON 数据。这份 JSON 数据中只有 “城市 > 省份 > 中国” 这样的层级数据,我们要将它与绘图指令建立联系。

建立联系指的是,我们要把数据的层级、位置和要绘制的圆的半径、位置一一对应起来。即我们要把数据转换成图形信息,这个步骤需要数学计算。不过,我们可以直接使用 d3-hierarchy (opens new window) 这个工具库转换数据。

通过转换后就能获得包含几何图形信息的数据对象。此时它的内部结构如下所示:

{
  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,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

我们需要的信息是数据中的 x、y、r,这些数值是前面调用 d3.hierarchy 帮我们算出来的。接下来我们只需要用 Canvas 将它们绘制出来就可以了。

只需要遍历数据并且根据数据内容绘制圆弧,分为两步:

  • 第一步:我们在当前数据节点绘制一个圆,圆可以使用 arc 指令来绘制。arc 方法的五个参数分别是圆心的 x、y 坐标、半径 r、起始角度和结束角度,前三个参数就是数据中的 x、y 和 r。因为我们要绘制的是整圆,所以后面的两个参数中起始角是 0,结束角是 2π。

  • 第二步,绘制图成后,如果这个数据节点有下一级数据,我们遍历它的下一级数据,然后递归地对这些数据调用绘图过程。如果没有下一级数据,说明当前数据为城市数据,那么我们就直接给出城市的名字,这一步可以通过 fillText 指令来完成。

最终代码如下,最终效果可以看这里 (opens new window)。此外,还可以实现鼠标移到某个城市对应的小圆高亮的效果 (opens new window)

<body>
  <canvas width="1600" height="1600" style="width: 800px; height: 800px;" />
</body>
<script type="module">
  import * as d3 from "https://cdn.skypack.dev/d3-hierarchy@3";
  const dataSource = "https://s5.ssl.qhres2.com/static/b0695e2dd30daa64.json";

  /* globals d3 */
  (async function() {
    const data = await (await fetch(dataSource)).json();
    // 将省份数据按照包含城市的数量,从多到少排序
    const regions = d3
      .hierarchy(data)
      .sum((d) => 1)
      .sort((a, b) => b.value - a.value);

    // 通过 d3.pack() 将数据映射为一组 1600 宽高范围内的圆形
    // 为了展示得美观一些,在每个相邻的圆之间保留 3 个像素的 padding
    const pack = d3
      .pack()
      .size([1600, 1600])
      .padding(3);

    const root = pack(regions);

    const canvas = document.querySelector("canvas");
    const context = canvas.getContext("2d");
    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(context, 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);
  })();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# Canvas 的优缺点

首先,Canvas 是一个非常简单易用的图形系统。Canvas 通过一组简单的绘图指令,就能够方便快捷地绘制出各种复杂的几何图形。

另外,Canvas 渲染起来相当高效。即使是绘制大量轮廓非常复杂的几何图形,Canvas 也只需要调用一组简单的绘图指令就能高性能地完成渲染。这点和 Canvas 更偏向于渲染层,能够提供底层的图形渲染 API 有关。因此在实际实现可视化业务的时候,Canvas 出色的渲染能力正是它的优势所在。

不过 Canvas 也有缺点,因为 Canvas 在 HTML 层面上是一个独立的画布元素,所以所有的绘制内容都是在内部通过绘图指令来完成的,绘制出的图形对于浏览器来说,只是 Canvas 中的一个个像素点,我们很难直接抽取其中的图形对象进行操作

比如说,在 HTML 或 SVG 中绘制一系列图形的时候,我们可以一一获取这些图形的元素对象,然后给它们绑定用户事件。但同样的操作在 Canvas 中没有可以实现的简单方法(不过我们仍然可以用其他方法和 Canvas 图形交互,比如要在前面绘制的层次关系图增加鼠标的交互,让鼠标指针在移动到某个城市所属的圆的时候,这个圆显示不同的颜色。可以获取鼠标坐标,然后用这个坐标到圆心的距离来判断)。

# 声明式图形系统:SVG

SVG (opens new window) 的全称是 Scalable Vector Graphics,可缩放矢量图,它是浏览器支持的一种基于 XML 语法的图像格式。SVG 具有以下特点:

  • SVG 与 PNG、JPEG 和 GIF 图片相比,拥有很小的尺寸

  • SVG 是矢量图,可以伸缩,适用各种分辨率

  • SVG 采用开放标准,本质是 XML,可以被各种工具读取(浏览器、图片管理器、markdown 等)

  • SVG 图像中的文本是可选的,同时也是可搜索的

  • SVG 可以与 CSS 结合,获得更强的扩展性

# 利用 SVG 绘制几何图形

比如绘制一个黑色边框的橙色的圆,最终效果可以看这里 (opens new window)

<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>
1
2
3
4
5
6
7
8
9
10

在这段代码中,svg 元素是 SVG 的根元素,属性 xmlns 是 xml 的名字空间。第一行代码就表示,svg 元素的 xmlns 属性值是 "http://www.w3.org/2000/svg (opens new window)",浏览器根据这个属性值就能够识别出这是一段 SVG 的内容了。

svg 元素下的 circle 元素表示这是一个绘制在 SVG 图像中的圆形,属性 cx 和 cy 是坐标,表示圆心的位置在图像的 x = 100、y = 50 处。属性 r 表示半径,r = 40 表示圆的半径为 40。默认的单位都是 px。

# viewport 和 viewBox 属性

  • viewport 是 svg 图像的可见区域。

  • viewBox (opens new window) 是用于在画布上绘制 svg 图形的坐标系统,如果设置了 viewBox 属性,那 SVG 内部的绘制就都是相对于这个坐标系的了。

结合使用 viewport 和 viewBox,就可以使得 svg 图标等比例缩放。

<svg
  width="500"
  height="200"
  viewBox="0 0 50 20"
  style="border: 1px solid #000000"
>
  <rect
    x="20"
    y="10"
    width="10"
    height="5"
    style="stroke: #000000; fill:none;"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上述这段代码的 viewBox 坐标系中 1 = 10px,相当于以下代码:

<svg width="500" height="200" style="border: 1px solid #000000">
  <rect
    x="200"
    y="100"
    width="100"
    height="50"
    stroke-width="10"
    style="stroke: #000000; fill:none;"
  />
</svg>
1
2
3
4
5
6
7
8
9
10

# preserveAspectRatio 属性

preserveAspectRatio (opens new window) 用于当 viewport 和 viewBox 宽高比不相同时,指定这个坐标系在 viewport 中是否完全可见,同时也可以指定它在 viewport 坐标系统中的位置。这是一个较难理解的概念,它相当于在 viewport 内部绘制了一个虚拟内框,它的默认值为:xMidYMid meet

<svg
  width="500"
  height="200"
  viewBox="0 0 200 200"
  style="border: 1px solid #000000"
  preserveAspectRatio="xMidYMid meet"
>
  <rect
    x="100"
    y="100"
    width="100"
    height="50"
    stroke-width="10"
    style="stroke: #000000; fill:none;"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上述配置的原理如下图:

svg_viewbox

<svg
  width="500"
  height="200"
  viewBox="0 0 200 200"
  style="border: 1px solid #000000"
  preserveAspectRatio="xMaxYMin meet"
>
  <rect
    x="100"
    y="100"
    width="100"
    height="50"
    stroke-width="10"
    style="stroke: #000000; fill:none;"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上述配置的原理如下图:

svg_viewbox

preserveAspectRatio 的第二个参数有两种取值:

  • meet: 保持宽高比并将 viewBox 缩放为适合 viewport 的大小。meet 模式下,svg 将优先采纳压缩比较小的作为最终压缩比

  • slice: 保持宽高比并将所有不在 viewport 中的 viewBox 剪裁掉。slice 模式下,svg 将优先采纳压缩比较大的作为最终压缩比

<svg
  width="500"
  height="200"
  viewBox="0 0 200 200"
  style="border: 1px solid #000000"
  preserveAspectRatio="xMidYMax slice"
>
  <rect
    x="100"
    y="100"
    width="100"
    height="50"
    stroke-width="10"
    style="stroke: #000000; fill:none;"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上述代码原理如下图:

svg_viewbox

<svg
  width="500"
  height="200"
  viewBox="0 0 200 200"
  style="border: 1px solid #000000"
  preserveAspectRatio="xMaxYMin slice"
>
  <rect
    x="100"
    y="100"
    width="100"
    height="50"
    stroke-width="10"
    style="stroke: #000000; fill:none;"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上述代码原理如下图:

svg_viewbox

如果 preserveAspectRatio 为 none,则不保存宽高比。缩放图像适合整个 viewbox,可能会发生图像变形。none 模式下,svg 将分别计算 x 和 y 轴的压缩比

<svg
  width="500"
  height="200"
  viewBox="0 0 200 200"
  style="border: 1px solid #000000"
  preserveAspectRatio="none"
>
  <rect
    x="100"
    y="100"
    width="100"
    height="50"
    stroke-width="10"
    style="stroke: #000000; fill:none;"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 定义和使用 SVG 组件

在 svg 中,使用 defs 标签和 symbol 标签(以前是用 g 标签)来定义一个 svg 组件,symbol 与 g 标签类似,但 symbol 可以拥有一个独立的 viewBox

<svg width="0" height="0">
  <defs>
    <!-- 可以定义多个图标 -->
    <symbol id="more" viewBox="0 0 100 100">
      <circle r="5" cx="20" cy="25" fill="currentColor" />
      <circle r="5" cx="20" cy="50" fill="currentColor" />
      <circle r="5" cx="20" cy="75" fill="currentColor" />
      <line
        x1="40"
        y1="25"
        x2="90"
        y2="25"
        stroke-width="8"
        stroke="currentColor"
      />
      <line
        x1="40"
        y1="50"
        x2="90"
        y2="50"
        stroke-width="8"
        stroke="currentColor"
      />
      <line
        x1="40"
        y1="75"
        x2="90"
        y2="75"
        stroke-width="8"
        stroke="currentColor"
      />
    </symbol>
    <symbol id="filledArrowRight" viewBox="0 0 100 100">
      <polyline points="20 10, 80 50, 20 90" fill="currentColor"></polyline>
    </symbol>
    <symbol id="..."></symbol>
    <symbol id="..."></symbol>
  </defs>
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

可以使用 use 标签来使用一个 svg 组件。

<svg width="50" height="50" style="color:red">
  <use href="#more"></use>
</svg>
1
2
3

# SVG 动画(CSS)

# transform 变换

1. translate 位移

<svg width="200" height="200" viewBox="0 0 200 200">
  <rect x="0" y="0" width="50" height="50" transform="translate(10,10)" />
</svg>
1
2
3

2. rotate 旋转

<svg width="200" height="200" viewBox="0 0 200 200">
  <rect
    x="0"
    y="0"
    width="50"
    height="50"
    transform="translate(50,50) rotate(30)"
  />
</svg>
1
2
3
4
5
6
7
8
9

3. skewX 和 skewY 斜切

<svg width="200" height="200" viewBox="0 0 200 200">
  <rect
    x="0"
    y="0"
    width="50"
    height="50"
    transform="translate(50,50) skewX(30)"
  />
</svg>
1
2
3
4
5
6
7
8
9

4. scale 缩放

<svg width="200" height="200" viewBox="0 0 200 200">
  <rect
    x="0"
    y="0"
    width="50"
    height="50"
    transform="translate(50,50) scale(.5)"
  />
</svg>
1
2
3
4
5
6
7
8
9

5. matrix 复杂变形

<svg viewBox="0 0 200 200">
  <rect x="10" y="10" width="30" height="20" fill="green" />

  <!--
  In the following example we are applying the matrix:
  [a c e]    [3 -1 30]
  [b d f] => [1  3 40]
  [0 0 1]    [0  0  1]

  which transform the rectangle as such:

  top left corner: oldX=10 oldY=10
  newX = a * oldX + c * oldY + e = 3 * 10 - 1 * 10 + 30 = 50
  newY = b * oldX + d * oldY + f = 1 * 10 + 3 * 10 + 40 = 80

  top right corner: oldX=40 oldY=10
  newX = a * oldX + c * oldY + e = 3 * 40 - 1 * 10 + 30 = 140
  newY = b * oldX + d * oldY + f = 1 * 40 + 3 * 10 + 40 = 110

  bottom left corner: oldX=10 oldY=30
  newX = a * oldX + c * oldY + e = 3 * 10 - 1 * 30 + 30 = 30
  newY = b * oldX + d * oldY + f = 1 * 10 + 3 * 30 + 40 = 140

  bottom right corner: oldX=40 oldY=30
  newX = a * oldX + c * oldY + e = 3 * 40 - 1 * 30 + 30 = 120
  newY = b * oldX + d * oldY + f = 1 * 40 + 3 * 30 + 40 = 170
  -->
  <rect
    x="10"
    y="10"
    width="30"
    height="20"
    fill="red"
    transform="matrix(3 1 -1 3 30 40)"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 实现环形进度条

关键知识点:

<style>
  .circle {
    animation: circle 5s linear infinite;
  }

  @keyframes circle {
    from {
      stroke-dasharray: 0 1069;
    }
    to {
      stroke-dasharray: 1069 0;
    }
  }
</style>
<body>
  <svg width="440" height="440" viewbox="0 0 440 440">
    <circle
      cx="220"
      cy="220"
      r="170"
      stroke-width="50"
      stroke="#D1D3D7"
      fill="none"
    />
    <circle
      class="circle"
      cx="220"
      cy="220"
      r="170"
      stroke-width="50"
      stroke="#00A5E0"
      fill="none"
      transform="matrix(0,-1,1,0,0,440)"
    />
  </svg>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

matrix

同样的方法我们实现一个矩形进度条,代码如下:

<style>
  .rect {
    animation: rect 5s linear infinite;
  }

  @keyframes rect {
    from {
      stroke-dasharray: 0 800;
    }
    to {
      stroke-dasharray: 800 0;
    }
  }
</style>
<body>
  <svg width="200" height="200" viewBox="0 0 200 200">
    <rect
      x="0"
      y="0"
      width="200"
      height="200"
      fill="none"
      stroke="grey"
      stroke-width="8"
    />
    <rect
      x="0"
      y="0"
      width="200"
      height="200"
      fill="none"
      stroke="blue"
      stroke-width="8"
      class="rect"
      transform="matrix(0,1,-1,0,200,0)"
    />
  </svg>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

matrix 计算方法:

matrix

# 实现 LOGO 描边

关键知识点:

  1. 下载任意 SVG 格式的 LOGO

  2. 获取 path 长度

const path = document.getElementById("taobao-logo");
const pathLen = path.getTotalLength(); // 6885
1
2
  1. 添加描边样式和动画
.taobao-path {
  fill: none;
  stroke: #333;
  stroke-width: 1;
  animation: taobao 5s linear infinite forwards;
}
@keyframes taobao {
  from {
    stroke-dasharray: 6885;
    stroke-dashoffset: 6885;
  }
  to {
    stroke-dasharray: 6885;
    stroke-dashoffset: 0;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# SVG 动画(SMIL)

SMIL (opens new window) 全称 Synchronized Multimedia Integration Language,它允许我们通过 HTML 标签实现动画效果,它可以用于:

  • 实现过渡动画

  • 实现补间动画

  • 动画颜色变换

  • 路径运动动画(CSS3 无法实现)

# 为什么要用 SMIL

实现动画的全新方式:

  • 无需 CSS

  • 无需 JS

  • 几个标签轻松实现复杂动画

# set 标签

实现属性的延迟设置

<svg width="400" height="400">
  <rect x="0" y="0" width="100" height="100" fill="red">
    <set attributeName="x" attributeType="XML" to="10" begin="1s" />
    <set attributeName="x" attributeType="XML" to="20" begin="2s" />
    <set attributeName="x" attributeType="XML" to="30" begin="3s" />
    <set attributeName="x" attributeType="XML" to="40" begin="4s" />
    <set attributeName="x" attributeType="XML" to="50" begin="5s" />
    <set attributeName="fill" attributeType="XML" to="blue" begin="6s" />
  </rect>
</svg>
1
2
3
4
5
6
7
8
9
10

# animate 标签

案例一:移动的小球

<svg width="500" height="200" viewBox="0 0 500 200">
  <circle cx="0" cy="0" r="30" fill="blue" stroke="black" stroke-width="1">
    <!-- fill 属性的默认值是 remove,表示小球运动结束后回归原位,如果不想小球回归原位,可以设置为 freeze -->
    <animate
      attributeName="cx"
      from="0"
      to="200"
      dur="5s"
      repeatCount="indefinite"
      fill="freeze"
    />
    <animate
      attributeName="cy"
      from="0"
      to="200"
      dur="5s"
      repeatCount="indefinite"
      fill="freeze"
    />
  </circle>
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

案例二:形状补间动画

<svg width="400" height="400">
  <polygon points="30 30 70 30 90 70 10 70" fill="#fcc" stroke="black">
    <animate
      attributeName="points"
      attributeType="XML"
      to="50 30 70 50 50 90 30 50"
      dur="5s"
      fill="freeze"
      repeatCount="1"
    />
  </polygon>
</svg>
1
2
3
4
5
6
7
8
9
10
11
12

# animateTransform 标签

<svg width="200" height="200" viewBox="0 0 200 200">
  <rect x="0" y="0" width="60" height="60" fill="red">
    <animateTransform
      attributeName="transform"
      begin="0s"
      dur="3s"
      type="scale"
      from="1"
      to="2"
      repeatCount="indefinite"
      fill="freeze"
    />
  </rect>
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# animateMotion 标签

案例一:按 path 轨迹运动的正方形

<svg width="200" height="200" viewBox="0 0 200 200">
  <rect x="0" y="0" width="10" height="10" fill="red">
    <animateMotion
      path="M 10 10 L 110 10 L 110 110 L 10 110 Z"
      dur="5s"
      rotate="auto"
      fill="freeze"
      repeatCount="indefinite"
    />
  </rect>
  <path
    id="motion-path"
    d="M 10 10 L 110 10 L 110 110 L 10 110 Z"
    fill="none"
    stroke="green"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

案例二:混合动画

<svg viewBox="0 0 200 200" width="200" height="200">
  <rect x="0" y="0" rx="0" ry="0" width="10" height="10" fill="red">
    <animateMotion
      id="forward-rect"
      path="M 10 10 L 110 10 L 110 110 L 10 110"
      dur="5s"
      rotate="0"
      fill="freeze"
      begin="0; backward-rect.end + 0.5s"
    />
    <animateMotion
      id="backward-rect"
      path="M 10 110 L 110 110 L 110 10 L 10 10"
      dur="5s"
      rotate="0"
      fill="freeze"
      begin="forward-rect.end + 0.5s"
    />
    <animate
      id="red-to-blue"
      attributeType="XML"
      attributeName="fill"
      begin="0; blue-to-red.end + 1s"
      from="red"
      to="blue"
      dur="5s"
      fill="freeze"
    />
    <animate
      id="blue-to-red"
      attributeType="XML"
      attributeName="fill"
      begin="red-to-blue.end + 1s"
      from="blue"
      to="red"
      dur="5s"
      fill="freeze"
    />
  </rect>
  <path
    d="M 10 10 L 110 10 L 110 110 L 10 110"
    fill="none"
    stroke-width="1"
    stroke="blue"
  />
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

案例三:点击变色或位移

<svg viewBox="0 0 200 200" width="200" height="200">
  <g id="rect1">
    <rect x="0" y="0" rx="0" ry="0" width="100" height="100" fill="red">
      <animate
        attributeType="XML"
        attributeName="fill"
        from="red"
        to="green"
        begin="rect1.click"
        dur="2s"
        fill="freeze"
      />
    </rect>
  </g>
  <animateTransform
    attributeType="XML"
    attributeName="transform"
    type="translate"
    from="0, 0"
    to="50, 50"
    begin="rect1.click"
    dur="2s"
    fill="freeze"
  />
  <rect x="0" y="100" width="100" height="100" fill="blue">
    <animate
      attributeType="XML"
      attributeName="fill"
      from="blue"
      to="green"
      begin="rect1.click"
      dur="2s"
      fill="freeze"
    />
  </rect>
</svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

实现细节

  • rotate: auto -> 0 可以更加流畅

  • begin: 可填入多个值,支持表达式

  • 多个 animateMotion 并行时,后者会覆盖前者

  • path 用法 (opens new window)

# 实现 Loading 组件

loading

# 实现 FlyBox 组件

flybox

# 利用 SVG 绘制层次关系图

最终效果可以看这里 (opens new window)

<h1 id="title"></h1>
<svg
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  width="800"
  height="800"
  viewBox="0 0 1600 1600"
></svg>
1
2
3
4
5
6
7
8
const dataSource = "https://s5.ssl.qhres2.com/static/b0695e2dd30daa64.json";

/* globals d3 */
(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);

  const svgroot = document.querySelector("svg");

  // 通过创建 SVG 元素,将元素添加到 DOM 文档里,让图形显示出来
  function draw(
    parent,
    node,
    { fillStyle = "rgba(0, 0, 0, 0.2)", textColor = "white" } = {}
  ) {
    const children = node.children;
    const { x, y, r } = node;
    // 与使用 document.createElement 方法创建普通的 HTML 元素不同,SVG 元素要使用 document.createElementNS 方法来创建
    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);
    circle.setAttribute("data-name", node.data.name);
    parent.appendChild(circle);
    // 遍历下一级数据,创建一个 SVG 的 g 元素,递归地调用 draw 方法
    // SVG 的 g 元素表示一个分组,我们可以用它来对 SVG 元素建立起层级结构
    // 而且,如果我们给 g 元素设置属性,那么它的子元素会继承这些属性
    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 });
      }
      group.setAttribute("data-name", node.data.name);
      parent.appendChild(group);
    } 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);
    }
  }

  draw(svgroot, root);

  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;
  }

  let activeTarget = null;
  svgroot.addEventListener("mousemove", (evt) => {
    let target = evt.target;
    if (target.nodeName === "text") target = target.previousSibling;
    if (activeTarget !== target) {
      if (activeTarget) activeTarget.setAttribute("fill", "rgba(0, 0, 0, 0.2)");
    }
    target.setAttribute("fill", "rgba(0, 128, 0, 0.1)");
    titleEl.textContent = getTitle(target);
    activeTarget = target;
  });
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

# SVG 和 Canvas 的不同点

1. 写法上的不同

以层次关系图的绘制过程为例来对比一下。

在绘制层次关系图的过程中,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 的渲染性能相对比较低

2. 用户交互实现上的不同

因为 SVG 的一个图形对应一个元素,所以我们可以像处理 DOM 元素一样,很容易地给 SVG 图形元素添加对应的事件来实现用户交互,可以让图形的用户交互非常简单。而利用 Canvas 对图形元素进行用户交互就没有那么容易了,不过可以用数学计算的方法来解决。

# 绘制大量几何图形时 SVG 的性能问题

虽然使用 SVG 绘图能够很方便地实现用户交互,但是 SVG 这个设计给用户交互带来便利性的同时,也带来了局限性。因为它和 DOM 元素一样,以 节点的形式呈现在 HTML 文本内容中,依靠浏览器的 DOM 树渲染。如果我们要绘制的图形非常复杂,这些元素节点的数量就会非常多。而节点数量多,就会大大增加 DOM 树渲染和重绘所需要的时间。事实上,在一般情况下,当 SVG 节点超过一千个的时候,就能很明显感觉到性能问题了。

对于 SVG 的性能问题,也是有解决方案的。比如说,我们可以使用虚拟 DOM 方案来尽可能地减少重绘,这样就可以优化 SVG 的渲染。但是这些方案只能解决一部分问题,当节点数太多时,这些方案也无能为力。这个时候,我们还是得依靠 Canvas 和 WebGL 来绘图,才能彻底解决问题。

那在上万个节点的可视化应用场景中,SVG 就真的一无是处了吗?当然不是。SVG 除了嵌入 HTML 文档的用法,还可以直接作为一种图像格式使用,所以我们可以将生成的 SVG 作为图像,然后绘制到 Canvas 上。因此,即使是在用 Canvas 和 WebGL 渲染的应用场景中,我们也依然可能会用到 SVG,将它作为一些局部的图形使用,这也会给我们的应用实现带来方便。

# SVG 实用工具

  • termtosvg (opens new window) 是一个用 Python 编写的 Unix 终端记录器,它能够将我们在终端进行的操作录制生成 SVG 文件。生成的 SVG 文件可以使用以下命令用浏览器打开:

    open -a "/Applications/Google Chrome.app" /var/folders/cy/s7gsltgs15b16hw0qrp9vrl80000gn/T/termtosvg_tm4ifsep.svg
    
    1

    下一次想使用 termtosvg 的时候,需要先进到上次创建的文件中:

    source .venv/bin/activate
    
    1
  • SVG Optimizer (opens new window) 是一个用于优化 SVG 文件的 Node.js 工具。

# GPU 与渲染管线:WebGL

# 图形系统是如何绘图的

一个通用计算机图形系统主要包括 6 个部分,分别是输入设备中央处理单元图形处理单元存储器帧缓存输出设备。绘图过程的示意图如下:

  • 光栅(Raster):几乎所有的现代图形系统都是基于光栅来绘制图形的,光栅就是指构成图像的像素阵列

  • 像素(Pixel):一个像素对应图像上的一个点,它通常保存图像上的某个具体位置的颜色等信息。

  • 帧缓存(Frame Buffer):在绘图过程中,像素信息被存放于帧缓存中,帧缓存是一块内存地址

  • CPU(Central Processing Unit):中央处理单元,负责逻辑计算

  • GPU(Graphics Processing Unit):图形处理单元,负责图形计算

首先,数据经过 CPU 处理,成为具有特定结构的几何信息。然后,这些信息会被送到 GPU 中进行处理。在 GPU 中要经过两个步骤生成光栅信息。这些光栅信息会输出到帧缓存中,最后渲染到屏幕上。

这个绘图过程是现代计算机中任意一种图形系统处理图形的通用过程。它主要做了两件事,一是对给定的数据结合绘图的场景要素(例如相机、光源、遮挡物体等等)进行计算,最终将图形变为屏幕空间的 2D 坐标。二是为屏幕空间的每个像素点进行着色,把最终完成的图形输出到显示设备上。这整个过程是一步一步进行的,前一步的输出就是后一步的输入,所以我们也把这个过程叫做渲染管线(RenderPipelines)。

# GPU

CPU 和 GPU 都属于处理单元,但是结构不同。形象点来说,CPU 就像个大的工业管道,等待处理的任务就像是依次通过这个管道的货物。一条 CPU 流水线串行处理这些任务的速度,取决于 CPU(管道)的处理能力。

实际上,一个计算机系统会有很多条 CPU 流水线,而且任何一个任务都可以随机地通过任意一个流水线,这样计算机就能够并行处理多个任务了。这样的一条流水线就是我们常说的线程(Thread)。

这样的结构用来处理大型任务是足够的,但是要处理图像应用就不太合适了。这是因为,处理图像应用,实际上就是在处理计算图片上的每一个像素点的颜色和其他信息。每处理一个像素点就相当于完成了一个简单的任务,而一个图片应用又是由成千上万个像素点组成的,所以,我们需要在同一时间处理成千上万个小任务。

要处理这么多的小任务,比起使用若干个强大的 CPU,使用更小、更多的处理单元,是一种更好的处理方式。而 GPU 就是这样的处理单元。

GPU 是由大量的小型处理单元构成的,它可能远远没有 CPU 那么强大,但胜在数量众多,可以保证每个单元处理一个简单的任务。即使我们要处理一张 800 * 600 大小的图片,GPU 也可以保证这 48 万个像素点分别对应一个小单元,这样我们就可以同时对每个像素点进行计算了。

# 着色器

WebGL (opens new window) 程序是一个 WebGLProgram 对象,它是给 GPU 最终运行着色器的程序。着色器是用 GLSL 这种编程语言编写的代码片段

在绘图的时候,WebGL 是以顶点和图元来描述图形几何信息的。

顶点就是几何图形的顶点,比如,三角形有三个顶点,四边形有四个顶点。图元是 WebGL 可直接处理的图形单元,由 WebGL 的绘图模式决定,有点、线、三角形等等。

所以,顶点和图元是绘图过程中必不可少的。

因此,WebGL 绘制一个图形的过程,一般需要用到两段着色器,一段叫顶点着色器(Vertex Shader)负责处理图形的顶点信息,另一段叫片元着色器(Fragment Shader)负责处理图形的像素信息

更具体点来说,我们可以把顶点着色器可以理解为处理顶点的 GPU 程序代码。它可以改变顶点的信息(如顶点的坐标、法线方向、材质等等),从而改变我们绘制出来的图形的形状或者大小等等。

顶点处理完成之后,WebGL 就会根据顶点和绘图模式指定的图元,计算出需要着色的像素点,然后对它们执行片元着色器程序。简单来说,就是对指定图元中的像素点着色。

WebGL 从顶点着色器和图元提取像素点给片元着色器执行代码的过程,就是生成光栅信息的过程,也叫光栅化过程。所以,片元着色器的作用,就是处理光栅化后的像素信息

比如,如果我们将图元设为线段,那么片元着色器就会处理顶点之间的线段上的像素点信息,这样画出来的图形就是空心的。而如果我们把图元设为三角形,那么片元着色器就会处理三角形内部的所有像素点,这样画出来的图形就是实心的。

注意

  • 因为图元是 WebGL 可以直接处理的图形单元,所以其他非图元的图形最终必须要转换为图元才可以被 WebGL 处理。比如,如果我们要绘制实心的四边形,我们就需要将四边形拆分成两个三角形,再交给 WebGL 分别绘制出来。

  • 无论有多少个像素点,片元着色器都可以同时处理。并行处理是片元着色器的一大特点,也是 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);,三角形的周长就缩小为原来的一半了。

在这个过程中,我们不需要遍历三角形的每一个顶点,只需要利用 GPU 的并行特性,在顶点着色器中同时计算所有的顶点就可以了。

2. 向片元着色器传递数据

除了计算顶点之外,顶点着色器还可以将数据通过 varying 变量传给片元着色器。然后,这些值会根据片元着色器的像素坐标与顶点像素坐标的相对位置做线性插值。线性插值可以让像素点的颜色均匀渐变

# WebGL 的坐标系

WebGL 的坐标系是一个三维空间坐标系,坐标原点是(0, 0, 0)。其中,x 轴朝右,y 轴朝上,z 轴朝外。这是一个右手坐标系。

# WebGL 的数据

WebGL 使用的数据需要用类型数组定义,默认格式是 Float32ArrayFloat32Array (opens new window) 是 JavaScript 的一种类型化数组 (opens new window)TypedArray (opens new window)),JavaScript 通常用类型化数组来处理二进制缓冲区

# 如何用 WebGL 绘制三角形

浏览器提供的 WebGL API 是 OpenGL ES 的 JavaScript 绑定版本,它赋予了开发者操作 GPU 的能力

这一特点也让 WebGL 的绘图方式和其他图形系统的 “开箱即用”(直接调用绘图指令或者创建图形元素就可以完成绘图)的绘图方式完全不同,甚至要复杂得多。可以总结为以下 5 个步骤:

1. 创建 WebGL 上下文

2. 创建 WebGL 程序(WebGL Program)

3. 将数据存入缓冲区

4. 将缓冲区数据读取到 GPU

5. GPU 执行 WebGL 程序,输出结果

下面是 WebGL 的绘图流程图:

下面是使用 WebGL 绘制三角形的例子,最终效果可以看这里 (opens new window)

// 创建 WebGL 上下文
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");

// 创建 WebGL 程序
// 顶点着色器
const vertex = `
    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);
    }
  `;

// 片元着色器
// 可以通过设置 gl_FragColor 的值来定义和改变图形的颜色
// gl_FragColor 是 WebGL 片元着色器的内置变量,表示当前像素点颜色,它是一个用 RGBA 色值表示的四维向量数据
const fragment = `
    precision mediump float;
    varying vec3 color;

    void main() {
      gl_FragColor = vec4(color, 1.0);
    }    
  `;

// 将两种着色器分别创建成 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 程序上
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
// 通过 useProgram 选择启用这个 WebGLPrgame 对象
// 这样,当我们绘制图形时,GPU 就会执行我们通过 WebGLProgram 设定的两个 shader 程序
gl.useProgram(program);

// WebGL 所需要的数据
const points = new Float32Array([-1, -1, 0, 1, 1, -1]);

// 创建一个缓存对象,将它绑定为当前操作对象,再把当前的数据写入缓存对象
const bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

// 把数据绑定给顶点着色器中的 position 变量
const vPosition = gl.getAttribLocation(program, "position"); // 获取顶点着色器中的 position 变量
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0); // 给变量设置长度和类型
gl.enableVertexAttribArray(vPosition); // 激活这个变量

// 把数据传入缓冲区以后,GPU 也可以读取绑定的数据到着色器变量了
// 接下来,只需要调用绘图指令,就可以执行着色器程序来完成绘制了

// 调用 gl.clear 将当前画布的内容清除
gl.clear(gl.COLOR_BUFFER_BIT);

// 调用 gl.drawArrays 传入绘制模式
// 选择 gl.TRIANGLES 表示以三角形为图元绘制,再传入绘制的顶点偏移量和顶点数量
// WebGL 就会将对应的 buffer 数组传给顶点着色器,并且开始绘制
gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# WebGL 有趣的案例

# 常见问题

# Canvas2D 跟 WebGL 渲染到底区别在哪里

canvas2d 绘制出来的图形最终也是渲染到 gpu 中的,那么它和 webgl 渲染到底区别在哪呢?为什么说 webgl 性能更好?threejs 中的自定义 shader 为什么会比 threejs 精灵性能更好?

canvas2d 绘图是通过自身的 api,gpu 是浏览器底层调用的,不受开发者控制。webgl 不一样,将数据写入帧缓冲之后,最终通过 WebGLProgram 来执行 shader 完成图形渲染,所以 webgl 能够自己控制 gpu 渲染。

有很多图形计算,webgl 是可以放在 shader 里面去计算的,这样比用 js 计算快,这就是 gpu 和 cpu 计算的区别。

webgl 在渲染大量元素的时候手段要比 canvas2d 多得多,比如说同时绘制几万个小圆形,因为图形都一样,自己写 webgl 的话,可以用 instanced drawing 的方式批量绘制,而 canvas2d 实现的话浏览器是不会帮你去这么做的。所以性能差别就明显了。

可以看看 spritejs (opens new window) 的文档中的示例 (opens new window)里面的 benchmark 子目录下的例子,里面有几个用 canvas2d 无法实现的 case。

另外为什么 threejs 的自定义的 shader 会更好,也是因为我们可以在 shader 中完成一些计算,还有 threejs 精灵默认的 shader 是根据材质生成的,里面的计算也是依照一些规则来给出的。自定义 shader 更灵活。

# Canvas 绘制出的图形为什么会不清晰

这个问题涉及到设备像素比 dpr。mac 和 iphone 的 dpr 是 2,也就是说如果在这样的设备上绘制 canvas,应当将它的画布坐标设置为样式坐标的 2 倍,才可以清晰地显示图像。浏览器的 window.devicePixelRatio 属性可以读取设备像素比。

# Canvas 画布限制大小的问题

Canvas 画布大小有限制,不同的浏览器不同,最新的 Chrome 下应该是不超过 16384 X 16384,单个宽高不超过 32767 像素,一般的可视化大屏足够用了。检测设备的 Canvas 大小可以用这个项目:canvas-size (opens new window)

# Canvas API 有几种上下文

现在浏览器的 canvas 一般有 webgl2webgl2d 三种上下文。它们并不是一个完整的 canvas api 规范,而是分成了 2d 规范webgl 规范webgl 规范是 opengl es 规范在 web 端的实现,其中 webgl2 对应 opengl es 3.0,而 webgl 对应的是 opengl es 2.0。

# Canvas 对字体单位的支持情况

canvas 对 css 字体单位都支持,不过是相对于画布坐标的,也就是说当画布坐标和样式坐标相同的时候,1rem 和 html 的 1rem 是完全一样的,如果画布坐标是样式坐标的 2 倍,那么 canvas 字体 1rem 看起来就只有 html 的一半大小。使用时根据具体情况来使用吧,在需要自适应设备的时候使用相对单位肯定是会方便一些的。

# 新技术 WebXR

WebXR (opens new window) 是一组标准,它们一起用于支持将 3D 场景渲染到硬件,这些硬件设计用于呈现虚拟世界(虚拟现实或 VR),或用于将图形图像添加到现实世界(增强现实或 AR)。

上次更新时间: 2023年09月16日 23:53:18