原创

canvas绘制飞线效果

在我们做的可视化大屏项目中,经常会遇到飞线的效果。 在我们的大屏编辑器中,可以通过拖拽+配置参数的方式很快就能够实现。下面是我们使用大屏编辑器实现的一个项目效果:

效果

中间地图就有飞线的效果。

抛开编辑器的快速实现不说,我们大致来说下canvas绘制飞线的大致原理。

贝塞尔曲线

飞线的路径主要是一个贝塞尔曲线,canvas绘制贝塞尔曲线比较容易。canvas支持绘制二次和三次,在本次示例中,主要还是绘制二次贝塞尔曲线为主。canvas中指定二次贝塞尔曲线路径的函数如下:

ctx.quadraticCurveTo(cpx, cpy, x, y);

有关贝塞尔曲线的基础知识,读者可以自行学习,此处不再赘述。

渐变实现

从图中,可以看出飞线的效果是淡入的效果,颜色并不是一致的,起点处颜色很淡,终点处颜色就比较浓厚。

怎么样能够实现这种效果呢? 答案就是渐变,我们知道,canvas支持线性渐变和放射渐变。但是这两种渐变似乎都不太适合曲线的路径。

事实上,我们会考虑使用线性渐变。因为飞线效果中,曲线的弯曲程度都不太大,所以使用线性渐变,曲线造成的差异,人眼是感觉不出来的。

嗯嗯,图形学就是欺骗的艺术。

只要在线的起点和终点创建一个线性渐变,起点的颜色非透明度是0,终点的非透明度是1即可达到目标。

示例代码如下:

  function createGradient(ctx,p0,p1){
           var grd = ctx.createLinearGradient(p0.x,p0.y,p1.x,p1.y);
           grd.addColorStop(0,'rgba(255,0,255,0)');
           grd.addColorStop(1,'rgba(255,0,255,1)');
           return grd;
   }
  ctx.beginPath();
  ctx.moveTo(P0.x,P0.y);
  ctx.quadraticCurveTo(Q01.x,Q01.y,B1.x,B1.y);
  ctx.lineCap = 'round';
  ctx.lineWidth =3;
  ctx.strokeStyle = createGradient(ctx,P0,P2);
  ctx.shadowColor = 'rgba(255,0,255,1)';
  ctx.shadowBlur = 5;
  ctx.stroke();
渐变效果

流动效果

流动效果就是线条从起点开始,慢慢飞到终点的效果。 技术角度来说,就是绘制二次曲线百分之几的一部分,百分比的数值从0增加到1,然后又回到0,周而复始。

代码如下:

        let percent = 0.0;       
        function render(){
            ctx.save();
            //按百分比绘制
            ctx.restore();
            percent += 0.005;
            if(percent > 1){
                percent = 0.;
            }
            requestAnimationFrame(render);
        }

问题的关键在于如何绘制贝塞尔曲线的一部分。 一种思路是使用二次贝塞尔曲线的公式,把曲线分成很多片段来进行模拟,然而这种方式的效率并不高。 其实可以使用插值的方式来获取一段贝塞尔曲线。代码如下:

 // 参考https://xiaozhuanlan.com/topic/9506147283#section0t
        let P0 = startPoint, P1 = controlPoint,P2 = endPoint;
        let Q01 = interpolation(P0,P1,percent),
            Q11 = interpolation(P1,P2,percent),
            B1 = interpolation(Q01,Q11,percent);

function interpolation(P0,P1,t) {

        var Q = {
            x: P0.x * (1 - t) + P1.x * (t),
            y: P0.y * (1 - t) + P1.y * (t),
        };
        return Q;
    } 
#### 二次贝塞尔曲线
我们知道二次贝塞尔曲线有三个点P0、P1、P2。二次贝塞尔曲线的表达方程如下:
B(t) = (1-t)<sup>2</sup> * P0 + 2t(1-t) * P1 + t<sup>2</sup> * P2
其中: $t \in $[0,1]

借助上面一次贝塞尔曲线的计算方法,可以通过以下步骤来确定二次贝塞尔曲线的B(t)点:
* 选定 $t \in $[0,1]
* 通过插值运算法则,在P0和P1所组成的线段上,计算出P0和P1点之间的插值点Q0,其中插值的比例值是t。根据插值规则有:length( P0, Q0 ) = length( P0, P1 ) * t
* 通过插值运算法则,在P1和P2所组成的线段上,计算出P1和P2点之间的插值点Q1,其中插值的比例是t。
* 通过插值运算法则,在Q1和Q2所组成的线段上,计算出P1和P2点之间的插值点B,其中插值的比例是t。
上述过程中计算出来的点B就是在曲线上面点。上述过程如下图所示:

![二次贝塞尔曲线的计算方法过程](https://upload-images.jianshu.io/upload_images/6271001-773f3bc73bb803f7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

从图中可以得出结论:
* 直线(Q0,Q1)和曲线相切于B点。

另外还有隐藏的结论:
* 曲线(P0,B)也是贝塞尔曲线,P0是曲线的起始点,B是曲线的终止点,而Q0是控制点
* 曲线(B,P2)也是贝塞尔曲线,B是曲线的起始点,P2是曲线的终止点,而Q1是控制点

上面两个结论会很有用,有了这个两个结论,前面“迭代(分片)”绘制部分贝塞尔的方法,可以用更加简单的方法替代,这在稍后详细说明。

如果将t的值从0过渡到1,不断计算点B,这些点的集合就可以组成一条二次贝塞尔曲线。下面图形动画复现了这个效果:
![二次贝塞尔曲线的计算方法过程](https://upload-images.jianshu.io/upload_images/6271001-f2133738c2ea60be.gif?imageMogr2/auto-orient/strip)





通过上面的方式,就可以绘制流动的飞线效果了,如下图所示:

![流动效果](https://upload-images.jianshu.io/upload_images/6271001-370710e91634ec07.gif?imageMogr2/auto-orient/strip)


## 加上阴影
默认线条的样式并不是很好看,如果加上阴影,可以让效果更加丰满。  加上阴影也很简单,代码如下:

ctx.shadowColor = 'rgba(255,0,255,1)';

ctx.shadowBlur = 5;

最终的飞线效果参考下图:

效果

结语

如果对可视化感兴趣,可以和我交流,微信541002349. 另外关注公众号“ITMan彪叔” 可以及时收到更多有价值的文章。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 三维组态部件动画解决方案前言分离模型发方案整体模型方案总结

    其中涉及到的设备有冷却塔、水泵、螺杆机、离心机 、分水器(集水器)、阀门,以及管路。 其中冷却塔,水泵,螺杆机,离心机都有停机/开机状态,开机状态下要有叶轮转动...

    用户3158888
  • 三维组态部件动画解决方案

    其中涉及到的设备有冷却塔、水泵、螺杆机、离心机 、分水器(集水器)、阀门,以及管路。

    用户3158888
  • 根据贝塞尔曲线上的点反算t值

    这是一个项目中遇到的实际需求。 场景是一个智能仓库管理系统,场景里面有直线和曲线构成的环穿轨道。环穿轨道上面会有小车运动,后台推动小车的两个点位A和B,其中A和...

    用户3158888
  • 疫情爆发,为什么人们第一时间把卫生纸给抢空了?

    到底是为什么每次一发生各种危机,人们就开始涌向超市去抢购那些“不能吃不能喝“,而且还廉价的卫生纸呢?

    悲了伤的白犀牛
  • Microsoft Visual Studio 2010编译生成总出现exe写入错误

    之前都是在Linux系统下运用GCC编译器调试程序,今天安装VS2010调试程序,感觉功能还是挺强大的,调试程序时,突然——发现用VS2010在编译正确后,...

    程序手艺人
  • Java面试题-集合框架篇三

    这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组...

    用户5224393
  • [ Java面试题 ] 集合篇

    Kevin_Zhang
  • HTML5常用的文本标签

        <h1>~<h6>用处是为了命名标题,<h1> 定义最大的标题。<h6>用来定义最小的标题;标题标签主要目的不是为了设置字体大小,主要作用是对浏览器索引...

    王凡汎
  • 一个工控漏洞引发的思考(续)

    阿基米德曾经说过:给我一个支点,我就能撬起地球。”那么,在漏洞挖掘的过程中,如果给你一个支点(pivot),能否快速发掘更多漏洞呢?上一篇分析了研华公司(Adv...

    FB客服
  • 2020-5-11-HATEOAS简介

    之前2020-5-6-restful理解 - huangtengxiao和大家介绍了对RESTful的理解。今天和大家介绍下RESTful中最重要的一个概念HA...

    黄腾霄

扫码关注云+社区

领取腾讯云代金券