基于html5canvas的拓扑组件开发

前端学习123321123      2022-02-17     507

关键词:

在现在前端圈大行其道的 React 和 Vue 中,可复用的组件可能是他们大受欢迎的原因之一,

在 HT 的产品中也有组件的概念,不过在 HT 中组件的开发是依托于 HTML5 Canvas 的技术去实现的,

也就是说如果你有过使用 Canvas 的开发经验你就可以来封装自己的组件。

下面我以一个进度环为例,来探究一下如何使用ht.js封装出一个拓扑组件。

效果图

技术图片

代码实现

前置知识

自定义组件

除了HT预定义的组件类型外,用户还可以自定义扩展类型,自定义有两种方式:

  • 直接将type值设置成绘制函数:function(g, rect, comp, data, view){}
  • 通过ht.Default.setCompType(name, funtion(g, rect, comp, data, view){})注册组件类型,矢量type值设置成相应的注册名

在这里我选用第一种通过形如

ht.Default.setImage(‘circle-progress-bar‘, {
    width: 100,
    height: 100,
    comps: [
        {
            type: function(g, rect, comp, data, view) {
                // ...
            }
        }
    ]
});

这样的方式完成组件的声明,那么 function(g, rect, comp, data, view) { }中的内容就是我们接下来需要关注的了

准备工作

  1. 抽象并声明出几个 Coding 中需要的变量

    • 进度百分比 progressPercentage {百分比}
    • 圆环渐变色 linearOuter {颜色数组}
    • 内圆渐变色 linearInner {颜色数组}
    • 字体缩放比例 fontScale {数字}
    • 显示原始值 showOrigin {布尔}
    • 进度条样式 progressLineCap {线帽样式}
  2. 变量的声明和赋值了

    var x = rect.x;
    var y = rect.y;
    var rectWidth = rect.width;
    var rectHeight = rect.height;
    var width = rectWidth < rectHeight ? rectWidth : rectHeight;
    var progressPercentage = parseFloat((data.a(‘progressPercentage‘) * 100).toFixed(10));
    var fontScale = data.a(‘fontScale‘);
    var showOrigin = data.a(‘showOrigin‘);
    var backgroundColor = data.a(‘backgroundColor‘);
    var progressLineCap = data.a(‘progressLineCap‘);
    var fontSize = 16; // 字体大小
    var posX = x + rectWidth / 2; // 圆心 x 坐标
    var posY = y + rectHeight / 2; // 圆心 y 坐标
    var circleLineWidth = width / 10; // 圆环线宽
    var circleRadius = (width - circleLineWidth) / 2; // 圆环半径
    var circleAngle = {sAngle: 0, eAngle: 2 * Math.PI}; // 绘制背景圆和圆环内圆所需的角度
    var proStartAngel = Math.PI; // 进度环起始角度
    var proEndAngel = proStartAngel + ((Math.PI * 2) / 100) * progressPercentage; // 进度环结束角度
  3. 创建渐变色样式

    var grd = context.createLinearGradient(x1, y1, x2, y2);
    grd.addColorStop(0, ‘red‘);   
    grd.addColorStop(1, ‘blue‘);

    在 Canvas 中的渐变色是按照如上方式来创建的,但是在一个组件中去如果一个一个去添加显然是去组件的理念是背道而驰的,所以我选择封装一个函数根据颜色数组中的各个颜色来生成渐变色样式

    // 创建渐变色样式函数
    function addCreateLinear(colorsArr) {
        var linear = rectWidth < rectHeight
            ? g.createLinearGradient(x, posY - width / 2, width, posY + width / 2)
            : g.createLinearGradient(posX - width / 2, y, posX + width / 2, width);
        var len = colorsArr.length;
        for (var key in colorsArr) {
            linear.addColorStop((+key + 1) / len, colorsArr[key]);
        }
        return linear;
    }
    // 创建渐变填充颜色
    var linearOuter = addCreateLinear(data.a(‘linearOuter‘));
    var linearInner = addCreateLinear(data.a(‘linearInner‘));

开始 Coding

准备工作结束后下面就是 Canvas 的时间了

  1. 绘制背景圆

    g.beginPath();
    g.arc(posX, posY, circleRadius, circleAngle.sAngle, circleAngle.eAngle);
    g.closePath();
    g.fillStyle = backgroundColor;
    g.fill();
    g.lineWidth = circleLineWidth;
    g.strokeStyle = backgroundColor;
    g.stroke();

    技术图片

  2. 绘制进度环

    g.beginPath();
    g.arc(posX, posY, circleRadius, proStartAngel, proEndAngel);
    g.strokeStyle = linearOuter;
    g.lineWidth = circleLineWidth;
    g.lineCap = progressLineCap;
    if (progressPercentage !== 0) g.stroke();

    技术图片

  3. 绘制中心圆

    g.beginPath();
    g.fillStyle = linearInner;
    g.arc(posX, posY, circleRadius - circleLineWidth / 2 - 1, 0, Math.PI * 2, false);
    g.strokeStyle = ‘#0A2E44‘;
    g.fill();
    g.lineWidth = 2;
    g.stroke();

    技术图片

  4. 绘制文字

    g.fillStyle = ‘white‘;
    g.textAlign = ‘center‘;
    g.font = fontSize + ‘px Arial‘;
    g.translate(posX * (1 - fontScale), posY * (1 - fontScale));
    g.scale(fontScale, fontScale);
    showOrigin
        ? g.fillText(progressPercentage / 100, posX, posY + fontSize / 3)
        : g.fillText(progressPercentage + ‘%‘, posX, posY + fontSize / 3);

    技术图片

    最后通过简单的配置就可以在网页上呈现出这个进度环了

    var dataModel = new ht.DataModel();
    var graphView = new ht.graph.GraphView(dataModel);
    var circle1 = new ht.Node();
    circle1.setPosition(150, 150);
    circle1.setSize(200, 200);
    circle1.setImage(‘circle-progress-bar‘);
    circle1.a({
        progressPercentage: 0.48,
        linearOuter: [‘#26a67b‘, ‘#0474d6‘],
        linearInner: [‘#004e92‘, ‘#000000‘],
        fontScale: 1,
        showOrigin: true,
        progressLineCap: ‘butt‘,
        backgroundColor: ‘rgb(61,61,61)‘
    });
    dataModel.add(circle1);
    // 这次多生成几个 不过代码相似 在此就不赘述了

    技术图片

    完整代码如下

    ht.Default.setImage(‘circle-progress-bar‘, {
        width: 100,
        height: 100,
        comps: [
            {
                type: function(g, rect, comp, data, view) {
                    // 获取属性值
                    var x = rect.x;
                    var y = rect.y;
                    var rectWidth = rect.width;
                    var rectHeight = rect.height;
                    var width = rectWidth < rectHeight ? rectWidth : rectHeight;
                    var progressPercentage = parseFloat((data.a(‘progressPercentage‘) * 100).toFixed(10));
                    var fontScale = data.a(‘fontScale‘);
                    var showOrigin = data.a(‘showOrigin‘);
                    var backgroundColor = data.a(‘backgroundColor‘);
                    var progressLineCap = data.a(‘progressLineCap‘);
                    var fontSize = 16;
    
                    // 定义属性值
                    var posX = x + rectWidth / 2;
                    var posY = y + rectHeight / 2;
                    var circleLineWidth = width / 10;
                    var circleRadius = (width - circleLineWidth) / 2;
                    var circleAngle = {
                        sAngle: 0,
                        eAngle: 2 * Math.PI
                    };
                    var proStartAngel = Math.PI;
                    var proEndAngel = proStartAngel + ((Math.PI * 2) / 100) * progressPercentage;
    
                    // 创建渐变背景色
                    function addCreateLinear(colorsArr) {
                        var linear = rectWidth < rectHeight ? g.createLinearGradient(x, posY - width / 2, width, posY + width / 2) : g.createLinearGradient(posX - width / 2, y, posX + width / 2, width);
                        var len = colorsArr.length;
                        colorsArr.forEach(function(item, index) {
                            linear.addColorStop((index + 1) / len, item);
                        });
                        return linear;
                    }
                    // 创建渐变填充颜色
                    var linearOuter = addCreateLinear(data.a(‘linearOuter‘));
                    var linearInner = addCreateLinear(data.a(‘linearInner‘));
    
                    // 0.保存绘制前状态
                    g.save();
    
                    // 1.背景圆
                    g.beginPath();
                    g.arc(posX, posY, circleRadius, circleAngle.sAngle, circleAngle.eAngle);
                    g.closePath();
                    g.fillStyle = backgroundColor;
                    g.fill();
                    g.lineWidth = circleLineWidth;
                    g.strokeStyle = backgroundColor;
                    g.stroke();
    
                    // 2.进度环
                    g.beginPath();
                    g.arc(posX, posY, circleRadius, proStartAngel, proEndAngel);
                    g.strokeStyle = linearOuter;
                    g.lineWidth = circleLineWidth;
                    g.lineCap = progressLineCap;
                    if (progressPercentage !== 0) g.stroke();
    
                    // 3.绘制中心圆
                    g.beginPath();
                    g.fillStyle = linearInner;
                    g.arc(posX, posY, circleRadius - circleLineWidth / 2 - 1, 0, Math.PI * 2, false);
                    g.strokeStyle = ‘#0A2E44‘;
                    g.fill();
                    g.lineWidth = 2;
                    g.stroke();
    
                    // 4.绘制文字
                    g.fillStyle = ‘white‘;
                    g.textAlign = ‘center‘;
                    g.font = fontSize + ‘px Arial‘;
                    g.translate(posX * (1 - fontScale), posY * (1 - fontScale));
                    g.scale(fontScale, fontScale);
                    showOrigin ? g.fillText(progressPercentage / 100, posX, posY + fontSize / 3) : g.fillText(progressPercentage + ‘%‘, posX, posY + fontSize / 3);
    
                    // 5.恢复绘制前状态
                    g.restore();
                }
            }
        ]
    });

几点心得

声明属性

在这个部分有几点可供参考

  • 使用小驼峰对属性进行命名,并且少用缩写尽量语义化

    举个栗子:

    • fontScale 字体缩放比例
    • progressPercentage 进度百分比
  • 属性值类型的选择也要尽量贴合属性的含义

    举个栗子:

    • 一个存储着几个颜色值字符串的数组,用颜色数组就比单纯的数组更为贴切
    • 一个表示画笔线帽种类的字符串,用线帽样式就比字符转更为贴切

使用属性

由于进度环是一个圆形的组件,那么在这里有两点供参考

  • 当组件的 rect.widthrect.height 不相等的时候我们需要自己来设定一个 width,

    让圆在这个以 width 为边的正方形中绘制,而 width 的值就是 rect.widthrect.height 中较短的一边,

    而这么做的理由是这样绘制圆自适应性能力会更好,并且圆心也直会在 (rect.width/2, rect.height/2)这一点上。

    var rectWidth = rect.width;
    var rectHeight = rect.height;
    var width = rectWidth < rectHeight ? rectWidth : rectHeight;
  • 由于我们自己设定了一个 width,那么在设置渐变颜色的参数上就需要注意一下了。

    当 rect.width 不等于 rect.height 的时候。

    如果按照 g.createLinearGradient(0, 0, rect.width, rect.height) 设置渐变色就会出现下面的效果,右下方的蓝色不见了。

技术图片

不过如果按照如下代码的方式设置渐变色就会出现下面的效果就会出现预期的效果了。

var posX = rectWidth / 2;
var posY = rectHeight / 2;
var linear = rectWidth < rectHeight
        ? g.createLinearGradient(0, posY - width / 2, width, posY + width / 2)
        : g.createLinearGradient(posX - width / 2, 0, posX + width / 2, width);

技术图片

原因其实很简单,就是渐变颜色方向的起点和终点并没有随着 width 的改变而改变。

如图所示以rectWidth > rectHeight 为例

技术图片

绘制组件

在绘制组件的过程中,我们需要把一些边界条件和特殊情况考虑到,来保持组件的扩展性和稳定性

下面就是一些我的心得

  • 在做了 g 操作的头尾分别使用 saverestore ,以此来保障 g 操作的不影响后续的扩展开发。

    g.save()
    // g 操作
    // ...
    // ...
    g.restore()
    save/restore

    设想一下,我们正在用 10 像素宽,颜色为红色的笔画图,然后把画笔设置成1像素宽,颜色变成绿色。绿色画完之后呢,我们想接着用10像素的红色来画,如果没有 save 与 restore,那我们就不得不重新设置一遍画笔——如果画笔状态过多,那我们的代码就会大量增加;而且,这些设置过程是重复而乏味的。

    最后保存的最先还原!restore 总是还原离他最近的 save 点(已经还原的不能第2次还原到他)。

    另外?save?和?restore?一般是改变了?transform?或?clip?才需要,大部分情况下不需要,例如你设置了颜色、宽度等等参数,下次要绘制这些的人会自己再设置这些,所以能尽量不用?save/restore?的地方可以尽量不用,那也是有代价的

  • 当进度值为 0 且 线帽样式为圆角的时候进度环会变成一个圆点,正确的做法使需要对进度值为 0 的时候进行特殊处理。

    // 进度环
    g.beginPath();
    g.arc(posX, posY, circleRadius, proStartAngel, proEndAngel);
    g.strokeStyle = linearOuter;
    g.lineCap = progressLineCap;
    if (progressPercentage !== 0) g.stroke();
  • 由于 Chrome 浏览器的限制(Chrome 显示最小字体为 12 px),所以不能通过 12px这样的数值设定文字大小,只能通过缩放来控制文字的大小了。

    当你高高兴兴的的使用 scale 对文字进行缩放的时候

    var fontScale = 0.75
    g.fillStyle = ‘white‘;
    g.textAlign = ‘center‘;
    g.font = fontSize + ‘px Arial‘;
    g.scale(fontScale, fontScale);
    g.fillText(progressPercentage + ‘%‘, posX, posY + fontSize / 3);

    你会得到这样的结果

    技术图片

造成这个结果的原因是 scale 操作的参考点位置不对

下面我们使用矩形的例子详细解释一下

// 原矩形
ctx.save();
ctx.beginPath();
ctx.strokeRect(0, 0, 400, 400);
ctx.restore();
// 缩放后的矩形
ctx.save();
ctx.beginPath();
ctx.scale(0.75, 0.75);
ctx.strokeRect(0, 0, 400, 400);
ctx.restore();

技术图片

这时 scale 的参考点是(0,0)所以,中心缩放没有按照我们预期的进行

当修改参考点的坐标为(50,50)之后,中心缩放就正常了

技术图片

那么这个(50,50)是怎么得来的?

根据上图我们不难看出这个距离其实就是 (缩放前的边长 - 缩放后的边长) / 2得到得

公式就是 width * (1 - scale) / 2

在这个例子中套用一下就是 400 * (1 - 0.75) / 2 = 50

// 原矩形
ctx.save();
ctx.beginPath();
ctx.strokeRect(0, 0, 400, 400);
ctx.restore();
// 缩放后的矩形
ctx.save();
ctx.beginPath();
ctx.translate(50, 50)
ctx.scale(0.75, 0.75);
ctx.strokeRect(0, 0, 400, 400);
ctx.restore();

我们把上面得公式在做进一步的扩展,让它的适用性更强

width * (1 - scale) / 2   -> width / 2 * (1 - scale)  -> posX * (1 - scale)
height * (1 - scale) / 2  -> height / 2 * (1 - scale) -> posY * (1 - scale)

在这里也需要明确一点 posX = x + (width / 2) posY = y + (height / 2)

在进一步抽象成函数

function centerScale(ctx, posX, posY, scaleX, scaleY) {
    ctx.translate(posX * (1 - scaleX), posY * (1 - scaleY));
    ctx.scale(scaleX, scaleY);
}

那么其中的文字缩放也是如出一辙

var fontScale = 0.75
g.fillStyle = ‘white‘;
g.textAlign = ‘center‘;
g.font = fontSize + ‘px Arial‘;
g.translate(posX * (1 - fontScale), posY * (1 - fontScale));
g.scale(fontScale, fontScale);
g.fillText(progressPercentage + ‘%‘, posX, posY + fontSize / 3);

当然结果也是很不错的??,文字的缩放功能实现了

技术图片

在实现上如果大家有什么问题可以直接留言或者私信或者直接去官网hightopo上查阅相关的资料

结语

这个进度环组件的开发就到此结束了,相信小伙伴们通过我的这篇学习笔记也是可以通过ht.js独立开发一个拓扑组件了。后续我还会不定期的分享我的学习心得,希望小伙伴们也能给出自己的建议。

基于html5canvas绘制的电信网络拓扑图

电信网结构(telecommunicationnetworkstructure)是指电信网各种网路单元按技术要求和经济原则进行组合配置的组合逻辑和配置形式。组合逻辑描述网路功能的体系结构,配置形式描述网路单元的邻接关系,即以交换中心(或节点)和... 查看详情

javascript都有哪些适合做网络拓扑图形展示的包

...合适的方法:jTopo(JavascriptTopologylibrary)是一款完全基于HTML5Canvas的关系、拓扑图形化界面开发工具包。  使用jTopo很简单,可以快速创建一些关系图、拓扑等相关图形化的展示。只要您的数据之间存在有关联关系,都可以使用j... 查看详情

快速创建html5canvas电信网络拓扑图

前言电信网络拓扑图确实实用性很强,最近有个项目是基于这个的,为了写得丰富一点,还加了自动布局已经属性栏部分,不过这个Demo真的写得很用心,稍微改改都能直接运用到项目中去,还可以进行数据交互,总之希望能对... 查看详情

基于html5canvas实现矢量工控风机叶轮旋转

之前在拓扑上的应用都是些静态的图元,今天我们将在拓扑上设计一个会动的图元——叶轮旋转。先看看最后我们实现的效果:http://www.hightopo.com/demo/...我们先来看下这个叶轮模型长什么样从模型上看,这个叶轮模型有三个叶片... 查看详情

18个基于html5canvas开发的图表库

...。HTML5规范引进了很多新特性,其中之一就是Canvas元素。HTML5Canvas提供了通过JavaScript绘制图形的方法,非常强大。今天,本文收集了一些非常好的基于HTML5Canvas的图表方案推荐给大家。?1.?VisualizeVisualize通过Javas 查看详情

基于jtopo的拓扑图设计工具库ujtopo

...些基本的场景jTopo(JavascriptTopologylibrary)是一款完全基于HTML5Canvas的关系、拓扑图形化界面开发工具包。官网地址:http://www.jtopo.cn1、首先定义画布、舞台、场景并对舞台和场景做一些设置等//节点varnodeList=[];//连接关系varlinkList=[];... 查看详情

基于html5canvas的电机控制面板

前言作为刚入门的小白,我尝试着一步一步的做这个面板,用这篇文章来记录自己的一些收获(毕竟我还是个菜鸟)以及代码的实现,希望能够帮到一些朋友。当然,如果有什么意见可以直接跟我说,大家一起交流才会进步!效... 查看详情

基于html5canvas的智能安防scada巡逻模块

基于HTML5Canvas的智能安防SCADA巡逻模块前言随着大数据时代的来临,物联网的日益发展,原先的SCADA系统本身也在求新求变,从最开始的专业计算机和操作系统,到通用计算机和相关软件,再到现在基于HTML5Canvas的新型组态开发,... 查看详情

前端入门:快速开发基于html5网络拓扑图应用

计算机网络的拓扑结构是引用拓扑学中研究与大小,形状无关的点、线关系的方法。把网络中的计算机和通信设备抽象为一个点,把传输介质抽象为一条线,由点和线组成的几何图形就是计算机网络的拓扑结构。网络的拓扑结构... 查看详情

快速开发基于html5网络拓扑图应用--入门篇

计算机网络的拓扑结构是引用拓扑学中研究与大小,形状无关的点、线关系的方法。把网络中的计算机和通信设备抽象为一个点,把传输介质抽象为一条线,由点和线组成的几何图形就是计算机网络的拓扑结构。网络的拓扑结构... 查看详情

html5canvas核心技术图形动画与游戏开发学习总结

save和restore函数的应用 保存canvas和恢复canvas clip函数的应用 文字的绘制 背景图片的绘制 离屏canvas 基于时间的运动 查看详情

基于唯一状态的前端组件开发

facebook的react的框架提出了一个基于唯一状态来渲染前端组件的想法。什么是唯一状态,採用唯一状态渲染究竟有什么优点。希望大家看到这篇文章以后不用不论什么框架也能够写出基于唯一状态渲染的前端组件。基于唯一状态... 查看详情

快速开发基于html5网络拓扑图应用之databinding数据绑定篇

...用这些绑定的数据做一篇说明,我写了一个简单的例子,基于机房工控的服务器上设备的灯闪烁现象。我们从2d和3d两个角度来分析数据绑定的问题。效果图2d代码实现其实不管是2d还是3d,在HT中,数据绑定不分维度的,所以两者... 查看详情

游戏开发中的基于组件的架构

】游戏开发中的基于组件的架构【英文标题】:ComponentBasedArchitectureingamedevelopment【发布时间】:2011-11-2904:56:10【问题描述】:我一直在考虑尝试基于组件的架构来进行游戏开发。我已经阅读了一些关于它的博客文章和文章,但... 查看详情

基于html5canvas实现的文字动画特效

前言文字是网页中最基本的元素,一般我们在网页上都是展示的静态文字,但是就效果来说,还是比较枯燥的。文字淡入淡出的动画效果在项目中非常实用,如果有某些关键的文字,可以通过这种动态的效果来提醒用户阅读。动... 查看详情

快速开发基于html5网络拓扑图应用--入门篇

上一篇我们绘制了一个graphView场景,在场景之上通过graphView.dm()获取数据容器,并通过graphView.dm().add()函数添加了两个Node节点,并通过setPosition设置节点位置以及setImage给节点添加图片;接着在两个节点之间通过ht.Edge(sourceNode,targe... 查看详情

基于html5canvas的3d热力云图效果

前言  数据蕴藏价值,但数据的价值需要用 IT 技术去发现、探索,可视化可以帮助人更好的去分析数据,信息的质量很大程度上依赖于其呈现方式。在数据分析上,热力图无疑是一种很好的方式。在很多行业中都有着... 查看详情

基于html5canvas的3d模型贴图问题

之前注意到的一个例子,但是一直没有沉下心来看这个例子到底有什么优点,总觉得就是一个list列表,也不知道右边的3d场景放两个节点是要干嘛,今天突然想起来就仔细地看了一下这个例子的代码,实际操作中应该还是有用处... 查看详情