在现在前端圈大行其道的 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);

    当然结果也是很不错的

    基于 HTML5 Canvas 的拓扑组件开发的更多相关文章

    1. 基于 HTML5 Canvas 的拓扑组件 ToolTip 应用

      前言 ToolTip 效果是网页制作中常见的使用特效.当用户将鼠标悬浮在某个控件上时,ToolTip 显示并向用户展示相应的提示信息:当鼠标离开时,ToolTip 隐藏.一般情况下,我们使用 Tool ...

    2. 18个基于 HTML5 Canvas 开发的图表库

      如今,HTML5 可谓如众星捧月一般,受到许多业内巨头的青睐.很多Web开发者也尝试着用 HTML 5 来制作各种各样的富 Web 应用.HTML 5 规范引进了很多新特性,其中之一就是 Canvas ...

    3. 基于html5 Canvas图表库 : ECharts

      ECharts开源来自百度商业前端数据可视化团队,基于html5 Canvas,是一个纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表.创新的拖拽重计算.数据视图.值 ...

    4. 基于 HTML5 Canvas 的智能安防 SCADA 巡逻模块

      基于 HTML5 Canvas 的智能安防 SCADA 巡逻模块 前言 最近学习了 HT for Web flow 插件,除了正常的 flow 效果,其中还有两个十分好用的两个接口 getPercen ...

    5. 基于html5 canvas和js实现的水果忍者网页版

      今天爱编程小编给大家分享一款基于html5 canvas和js实现的水果忍者网页版. <水果忍者>是一款非常受喜欢的手机游戏,刚看到新闻说<水果忍者>四周年新版要上线了.网页版 ...

    6. 基于HTML5 Canvas实现用户交互

      很多人都有这样的疑问,基于HTML5 Canvas实现的元素怎么和用户进行交互?在这里我们用到HT for Web(http://www.hightopo.com/guide/guide/core/b ...

    7. jmGraph:一个基于html5的简单画图组件

      jmGraph:一个基于html5的简单画图组件 特性: 代码书写简单易理解 面向对象的代码结构 对图形控件化 样式抽离 模块化:入seajs实现模块化开发 兼容性:暂只推荐支持html5的浏览器:i ...

    8. 基于HTML5 Canvas和jQuery 的绘图工具的实现

      简单介绍 HTML5 提供了强大的Canvas元素.使用Canvas并结合Javascript 能够实现一些很强大的功能.本文就介绍一下基于HTML5 Canvas 的绘图工具的实现.废话少说,先看成 ...

    9. 基于HTML5 Canvas实现的图片马赛克模糊特效

      效果请点击下面网址: http://hovertree.com/texiao/html5/1.htm 一.开门见山受美国肖像画家Chuck Close的启发,此脚本通过使用HTML5 canvas元素 ...

    随机推荐

    1. Qt获取控件位置,坐标总结

      总结的结果是: QMouseEvent中两类坐标系统,一类是窗口坐标,一类是显示器坐标.   总结一:经过试验,QMouseEvent::globalPos()  和 QCursor::pos()效果 ...

    2. 系统测试用例评审checklist

      规则要素内容 使用范围 审查结果 “否”的理由 “免”的理由 规则 建议 是 否 免 规范性规则               用例是否按照公司规定的模板进行编写?  √             用例的 ...

    3. flask多线程多协程操作

      local的作用:各个线程各开辟一块空间互不影响 基于local""" import threading from threading import local impo ...

    4. 轻松应对IDC机房带宽突然暴涨问题

      轻松应对IDC机房带宽突然暴涨问题! 1[提出问题] [实际案例一] 凌晨3:00点某公司(网站业务)的一个IDC机房带宽流量突然从平时高峰期150M猛增至1000M,如下图: 该故障的影响:直接导致 ...

    5. d3js selections深入理解

      D3 selections选择DOM元素以便可以对这些dom元素做相应的操作,比如:更改其style,修改其属性,执行data-join操作,或者插入.删除相应elements 比如,如果给定5个ci ...

    6. 基于bootstrap模态框的alert弹窗

      完成的效果如下: html代码: <!-- 弹出框 --> <div class="modal fade" id="alert_like" t ...

    7. Linux常用基本指令——文件处理命令

      书籍方面的推荐就不做介绍,免得别人说我有广告嫌疑.大家可以直接上百度,书籍和视频遍地都是,Linux这些方面的知识都是自己在学习视频和看书的总结,内容上可能会不完美.如果有更多的见解,欢迎直接评论. ...

    8. .NET中怎么有效的使用Cache

      Cache 即高速缓存 ,我想非常多人对他的第一印象一定像我相同,感觉他一定能提高系统得性能和运行速度.的确.Net推出cache的初衷确实是这样的.那么cache是怎么提高系统性能和运行速度呢?是不 ...

    9. 乘风破浪:LeetCode真题_001_TwoSum

      乘风破浪:LeetCode真题_001_TwoSum 一.前言 沉寂了很长时间,也悟出了很多的道理,写作是一种业余的爱好,是一种自己以后学习的工具,是对自己过往的经验积累的佐证,是检验自己理解深入度的 ...

    10. December 22nd 2016 Week 52nd Thursday

      The best hearts are always the bravest. 心灵最高尚的人,往往也是最勇敢的人. Keep conscience clear, don't let too many ...