可视化研发之线的画法:直线,曲线,动画(Canvas版)

“线”是可视化展示中最多见的图形元素,最直观的就是折线图,如图一。前端

图一 折线图git

一条线由多个点来定义,按照点与点之间的链接方式,一般将线分为“折线”和“曲线”,在画法上又分为“实线”和“虚线”,如图二:github

图二 折线和曲线web

咱们也常用线来绘制闭合的路径,从而造成可填充区域,好比面积图和雷达图,如图三和图四。算法

图三 面积图sql

图四 雷达图canvas

本篇文章在 Canvas API 的基础上,为你们讲解可视化研发中线的画法封装和线的动画实现方案(总体方案创建在图形学基础上,一样适用于WegGL 和3D场景)。api

* 0.1 线的定义

前面咱们提到过线的基本组成单位是“点(Point)”,两个相邻的点链接在一块儿成为一个“段(Segment)”,多个段拼装在一块儿组成一条线。如图六,这条线由7个点划分红的6个段组合而成。markdown

图六 点和段echarts

曲线的每一个段的起止点会由于插值算法的不一样而不一样,后面咱们会详细介绍。

图七所示的伪代码展现了咱们对线的基本定义。

图七 线的定义

线的绘制是以段为单位的,不一样的形状的线对段的拆分逻辑和画法都是有区别的,咱们从最简单的折线开始。

0.2 折线画法

0.2.1 获取段

折线对段的拆分很简单,根据传入的点数据,相邻两点划为一段。

图八 折线段的拆分

如上面的代码,实现很简单,依次遍历点数据,初始化段对象。这里有一个计算段的长度的操做,段的长度在动画场景是必须参数,在非动画场景则能够不用关心。折线的段的长度计算,就是计算一个线段的长度(两点间距离),如图九所示。

图九 线段的距离计算

另外图八的代码中,有一段是不是空段的判断逻辑。在实际的线图应用中,咱们在某些状况须要隐藏线的某些段,好比传入了空数据或者用户指定了过滤条件。

图十 空段

0.2.2 Canvas中线段的画法

在Canvas中画线段只须要两个api——moveTo 和 lineTo。图十一展现了链接[(0,0),(300,150),(400,150)]三个点的折线。

图十一  moveTo 与 lineTo

从上面的示例能够看到,Canvas 中绘制线段,只须要经过moveTo将画笔(Canvas 绘图上下文)定位到线段的起点,而后经过lineTo 绘制到线段的终点便可。多个首位相接的线段能够省略moveTo,直接lineTo。 要实现图十的空段效果,只须要moveTo到新段的起点便可,例如:

图十二  绘制空段

理解了基本的api以后,咱们回到咱们的折线上来,看看以段为单位的绘制方法。

0.2.3 折线绘制

基于上面画线的方法,咱们只须要遍历一条线中的全部段,依次链接就能够了。为了处理空段的绘制,设置一个lineStart的标记变量,若是处于start状态,会先moveTo到新的点,而不是lineTo。大体的绘图流程以下:

图十三 线的绘制基本流程

drawSegment方法以下:

图十四 drawSegment

这里你可能要疑惑,这里将线拆成段并无什么优点,为何不直接链接各个点呢?分红段完成了一个线的绘制的骨架,在这个骨架基础上,不少功能都会很容易的扩展。好比,线的每一段都有不一样的含义,可视化层面要展示这些不一样的含义须要给线赋予不一样的样式。这里咱们能够给LineSegment配置一个LineSegConfig,独立配置每一个段的样式,在绘制的过程当中若是发现新的段的样式发生了变化,就能够当即进行渲染,而后开始绘制新段,灵活拼装。好比下图,末尾的红色虚线用来表示预测数据。

图十五  分段渲染不一样样式

另外,分段会大大下降动画效果的实现成本,后面咱们详细介绍。

了解了折线的基本画法以后,咱们来看看曲线。

0.3 曲线画法

0.3.1 贝塞尔曲线

曲线有不少种,画曲线的方法也有不少种。因为Canvas 支持贝塞尔二次和三次曲线画法,曲线图表一般使用三次贝塞尔曲线画法,本文也将重点放在三次贝塞尔曲线的应用讲解上。那么什么是贝塞尔曲线呢?

Bézier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线。贝塞尔曲线点的数量决定了曲线的阶数,通常N个点构成的N-1阶贝塞尔曲线,即3个点为二阶。通常咱们都会要求曲线至少包含3个点,由于两个点的贝塞尔曲线是一条直线。按顺序,第一个点为 起点 ,最后一个点为 终点 ,其他点都为 控制点

下面咱们以二次贝塞尔曲线为例,讨论其生成过程。

二次贝塞尔曲线

给定点P0,P1,P2 ,P0 和 P2 为起点和终点,P1为控制点。从P0到P2的弧线即为一条二次贝塞尔曲线。

图十六 二次贝塞尔曲线

在这里咱们要将整个曲线的绘制量化为从0~1的过程,用t为当前过程的进度,t的区间即0~1。每一条线都须要根据t生成一个点,以下图,一个点从P0移动到P1,这是这条线从0~1的过程。

下面咱们还原一下一个二次贝塞尔曲线的生成过程。

图十七 绘制二次贝塞尔曲线(1)

如图十七,首先咱们连接P0P1,P1P2,获得两条线段。而后咱们对进度t进行取值,好比0.3,取一个Q0点,使得P0Q0的长度为P0P1总长度的0.3倍。

图十八 绘制二次贝塞尔曲线(2)

同时咱们在P1P2上取一点Q1,使得  P0Q0: P0P1 = P1Q1: P1P2。接下来咱们再在Q0Q1上取一点B,使得  P0Q0: P0P1 = P1Q1: P1P2 = Q0B:Q0Q1,如图十九

图十九 绘制二次贝塞尔曲线(3)

如今咱们获得的点B就是二次贝塞尔曲线的上的一个点,若是咱们使t=0开始取值,逐步递增进行插值,就会获得一系列的点B,进行链接就会造成一条完整的曲线,如图二十。


图二十 二次贝塞尔曲线绘制过程

上面展现了完整的二次贝塞尔曲线的产生过程,这个过程咱们通过数学推导,最终能够获得以下公式:

根据这个公式,咱们只要变动t值,就能够获得对应的点。

三次贝塞尔曲线

对应的,三次贝塞尔曲线由四个点组成,经过更多的迭代步骤来肯定曲线的上点,如图二十一所示。完整的生成若是如图二十二所示。

图二十一 三次贝塞尔曲线


图二十二 三次贝塞尔曲线生成过程

三次贝塞尔曲线的数学公式为:

0.3.2 Canas中如何绘制贝塞尔曲线

在canvas中绘制二次贝塞尔曲线使用的是 quadraticCurveTo  函数,参数定义以下:

函数只定义了控制点和终点,起点须要咱们使用moveTo来肯定,如图二十三的代码示例。

图二十三 canvas绘制二次贝塞尔曲线

三次贝塞尔曲线使用 bezierCurveTo() 方法来绘制,参数定义以下:

和二次曲线的绘制方式相似,如图二十四。

图二十四 canvas绘制三次贝塞尔曲线

下面的动图展现了控制点对贝塞尔曲线形状的影响。


图28 控制点对贝塞尔曲线的影响

0.3.3 样条曲线 与 获取段

咱们了解了如何绘制三次贝塞尔曲线,可是回到咱们的线图,一个线图会有不肯定数量的点被平滑的链接起来,可是目前三次贝塞尔曲线显然没法知足这个需求。咱们前面谈到了分段的概念,一条完整的曲线被分红了多段,若是每一段都是一条三次贝塞尔曲线,问题就解决了。那么问题就转化成了如何构造多条能依次平滑拼接的贝塞尔曲线。在图形学中有个概念叫“样条曲线”,专业的概念有点难懂,咱们这里简单理解就是将一个点的集合,分红多段曲线,各曲线处的链接点处有能够平滑链接(有连续的一次和二次导数)。关于样条曲线的连续性以及贝塞尔曲线的更多特性,读者能够参考《计算机图形学(第四版)》一书第14章——《样条表示》,这里咱们就不深刻解释了,直接看例子。

图29 一段由四条三次贝塞尔曲线拼接而成的曲线

以图29为例,如我咱们要将这条曲线分红四条三次贝塞尔曲线,咱们要肯定两个参数:

  1. 每条三次贝塞尔曲线的起点和终点
  2. 每条三次贝塞尔曲线的两个控制点

只有选取合适的起点、终点和控制点,咱们才能使得相邻的两条曲线能够平滑链接。样条曲线的拆分算法有不少种,这里也不详细介绍了,感兴趣的同窗能够参考图形学相关书籍;JavaScript 实现能够参考 d3-shape 的 Curves 接口(github.com/d3/d3-shape),d3-shape Curves 中的curveBasis、curveBasisClosed、curveBasisOpen、curveBundle、curveCardinal、curveCardinalClosed、curveCardinalOpen、curveCatmullRom、curveCatmullRomClosed、curveCatmullRomOpen、curveNatural、curveMonotoneXcurveMonotoneY都是基于三次贝塞尔曲线的样条实现。

下面咱们以Basis 算法的实现为例,进行讲解曲线如何获取“段”。

主流程

Basis 算法要求点集中的点的数量至少为3个,而后咱们利用以下逻辑进行段的获取:

  • 图30 获取曲线的 “段” 的主流程
  • 咱们的主流程逻辑很简单,循环给到的点,从当前索引位置开始向后取出3个点,而后根据这三个点以及当前段的起始点计算结束点和控制点。每一个新段的起点是上一个相邻段的终点。随后计算当前段的长度。 当前的循环逻辑不会计算到最后一个点,因此会少一个段,最后加个单独的逻辑来处理。

点的计算

下面来看看 Basis 算法点的计算:

  • 图31  basis 样条算法

如图31,咱们基于很简单的公式来计算各个点的值,这个公式是怎么来的呢?简单说是结合了B样条曲线和三次贝塞尔曲线在端点处的一阶和二阶导出得来的。这里就不深刻了,不然本篇文章会严重偏离主题,感兴趣的读者请参阅计算机图形学相关书籍。总之,咱们经过公式计算能够获得咱们须要的点。

曲线分割与长度计算

计算曲线的长度并非一件容易的事情,因为贝塞尔函数是插值函数,因此计算方法就是先对曲线进行切割,切割到足够小的范围,而后计算这一小段的曲线近似长度,再累加。0.3.1节给出了三次贝塞尔曲线的函数,咱们只须要将变量t取足够小的值,而后计算两个点之间的直线距离进行累加就能够,可是这种方案的性能消耗比较大。我在

community.khronos.org/t/3d-cubic-…

看到一种近似方法,利用该方法能够缩减切割次数。 基于三次贝塞尔曲线的函数,对一个贝塞尔曲线进行切割,很简单。咱们再把图21拿来讲明一下各点的计算。

图21

第一步:找到链接点

如图21,假设我要在t=0.25的位置将当前曲线切分红两条曲线,首先咱们要知道点B的位置。根据公式带入便可:

图33 根据t计算3次贝塞尔的点

第二步:获取控制点

拿到点P以后,P就是第一段的终点,第二段的起点,这样咱们只须要计算控制点便可。根据咱们以前对贝塞尔曲线绘制过程的理解,咱们能够得出以下结论:

  1. 第一段曲线的第一个控制点的运动轨迹是线段P0P1,和t线性相关
  2. 第一段曲线的第二个控制点的运动轨迹是线段Q0Q1,和t线性相关
  3. 第二段曲线的第一个控制点的运动轨迹是线段Q1Q2,和t线性相关
  4. 第二段曲线的第二个控制点的运动轨迹是线段P2P3,和t线性相关

依据上面的结论,三次贝塞尔曲线拆分的方法就很容易实现了:

图34 贝塞尔曲线拆分

图34 所示代码中 pointAt 方法为根据t获取直线上点的方法。以下:

图35 根据t获直线上的点

第三步 长度计算

咱们能够在任意位置对三次贝塞尔曲线进行拆分了,结合二分法,控制迭代次数,结合近似长度计算函数,咱们能够获得想要精度的长度值了。如图36。

图36  三次贝塞尔曲线的分割

获取段

内部细节咱们都梳理清楚了,获取全部的段也很简单了。如今须要特殊处理的是最后一个点数据,这里咱们将第二个点和第三个点都用最后一个点表示。

图37  basis 最后一段生成方法

0.3.4  曲线画法

关于曲线的全部准备工做都完成了,下面咱们要把它画出来。和画折线的方法相似,咱们只须要循环调用"段" 的绘制方法进行绘图便可。内部,只须要调用bezierCurveTo便可。以下:

图38 绘制曲线的段

0.4 动画

咱们完成了折线和曲线的绘制,想要线经过动画的方式画出来,只须要作少许的改动。首先不论直线仍是曲线咱们都分红了多段,每一段都是和t相关的函数。

0.4.1  基本方案

动画和非动画的本质区别就是一次画多少的问题,咱们将整条线图的绘制放置在[0,1]区间内,启动一个动画循环,每次绘图的时候更新的t的值,在咱们上面循环绘制segment 的代码中,将整条线图的t转化为每个段内部的t值。段 内部根据传入的t值,对自身进行切割,只画应该绘制的那部分。

图39  t值换算

由于咱们已经计算了每一个段的长度,和总长度,因此每一个段的占比由长度能够得到,此占比在和整个线图的t值进行换算便可。

以图39为例,好比咱们传入的t值为0.1,整条线图的0.1 换算到第一个段是0.4,那么第一个段只需绘制前40% 部分便可。咱们在图39的基础上,作少许的改动。

图40  支持局部绘制

如图40,咱们将外部计算的t(percent)传入绘制段的方法内,该方法会使用咱们以前介绍过的 divideCubic 方法对当前曲线进行切割,而后进行局部绘制。效果以下:


图41  动画

0.4.2  和其余动画方案的对比

实现线和面积的动画的方案还有总体Clip和生成点集两种方案,下面咱们简单对比一下,以说明咱们的分段绘制的优点。

方案 简介 函数调用 基于曲线的轨迹动画 不规则线 分段扩展
预生成点集 是利用曲线函数,预生成足够密度的点,而后将各点链接 较多。会产生大量的绘图函数的调用 支持 支持 能够支持,比较麻烦,也要有段的概念
总体clip 绘制以前设置一个裁剪窗口,调整裁剪窗口的大小来实现动画 较少 不支持,不能动态计算当前t值的x,y 不支持。只能在一个方向上clip,不能照顾x,y坐标值无序状况。 不支持
分段模型 一个图最多调用n-1 次 支持 支持 支持

0.4.3  动画同步

上面咱们看到的动画不一样的线之间虽然能够再同一时间到大终点,可是过程当中在x方向的位移是不一样步的。同步和不一样步都各有需求,尤为是在面积图状况下,单个面积图实际被拆分了上下两组segment。如图41.

图41 基本面积图的segement 

咱们观察上面面积图的绘图动画,它是从左到右推动的,好比当前的t值绘制到图41的矩形框的位置,那么首先会绘制第一段,计算第12段应该被绘制的区间,最后填充上下两段的闭合区间。这里有一个问题,若是是相同的t值,带入1和12的函数,产生的x值是不同的,那么绘制出来的效果就不对了,切面多是斜的。

解决这个问题作法是根据x或者y值反求t值,再带入目标函数中。对于三次贝塞尔曲线来讲,这又是一个大难题,因为篇幅所限及代码实现的比较复杂,这里就再也不讲解了,你们能够参考文后的参考资料。

0.5 参考资料:

一个超酷的贝塞尔类库:pomax.github.io/bezierjs/

一本超级棒的贝塞尔电子书 pomax.github.io/bezierinfo/

关于根据x或y反算t的讨论:www.zhihu.com/question/30…

图形学必读书物:《计算机图形学》

本文例子来源(字节跳动自研图表库):bytecharts.web.bytedance.net/


数据平台前端团队,在公司内负责风神、TEALibraDorado等大数据相关产品的研发。咱们在前端技术上保持着很是强的热情,除了数据产品相关的研发外,在数据可视化、海量数据处理优化、web excel、sql编辑器、私有化部署、工程工具都方面都有不少的探索和积累,有兴趣能够与咱们联系。对产品有任何建议和反馈也能够直接找咱们进行反馈~

欢迎关注「 字节前端 ByteFE 」简历投递联系邮箱「 tech@bytedance.com 」
复制代码
相关文章
相关标签/搜索