原生Canvas实现一个温度计UI组件

该温度计UI组件全部使用原生Cnavas的API完成,可以应用于物联网可视化、嵌入式Web上位机、大屏可视化等场景。

效果展示

随着温度升高,表示温度的矩形条在升高的过程中,会进行颜色渐变,这样在视觉上可以更直观的表达高低温状况。

参数化设计(尺寸之间相互关联绑定)

使用Canvas绘制复杂UI组件的时候,为了UI组件大小可以整体缩放,而不是某个参数变大,所有参数都要手动设置变大。对于解决这个问题,首先要声明一个变量或属性R,先设置一个尺寸值,对于canvas而言就是像素值,在编程的过程中所有的长度尺寸,都要以R为参考,形成特定的函数关系,只要R改变,所有的尺寸参数全部跟着改变。

基准值R的选择一般可以在你要设计的UI组件中,选择某个部位的尺寸作为基准值,这样该UI组件所有部位的值都与该部位形成简单的倍数关系。

{
  R: 30, // 参数化尺寸基准值  温度计底部圆圈半径
  ...
}

温度计外部轮廓绘制

外部轮廓是下面一个大的圆弧,上面一个小的半圆弧,然后加上两条直线,可以先用Canvas的圆弧和直线绘制出来轮廓,然后通过.lineWidth属性设置线条轮廓的宽度。

c.lineWidth = obj.R / 10;
c.strokeStyle = obj.borderColor;
c.beginPath();
// 起始角度
var startAngle = -Math.PI / 3;
var endAngle = Math.PI - startAngle; //与开始角度y轴轴对称角度
//温度计外轮廓底部圆弧
c.arc(0, 0, obj.R, startAngle, endAngle, false);
c.stroke(); //渲染填充轮廓
// 圆弧起点
var startX = obj.R * Math.cos(startAngle)
var startY = obj.R * Math.sin(startAngle)
// 温度计整体高度
var L = obj.R * 12
//温度计外轮廓右侧直线
c.moveTo(startX, startY);
c.lineTo(startX, startY - L);
c.stroke();
//温度计外轮廓左侧直线
c.moveTo(-startX, startY);
c.lineTo(-startX, startY - L);
c.stroke();
c.beginPath();
//温度计外轮廓顶部圆弧
var startAngle = -Math.PI / 3;
var endAngle = Math.PI - startAngle; //与开始角度y轴轴对称角度
//温度计外轮廓底部圆弧
c.arc(0, startY - L, startX, 0, Math.PI, true);
c.stroke(); //渲染填充轮廓

内部温度条

内部温度条可以使用一个圆形叠加一个矩形,圆形部分是静态的,矩形条是动态的,矩形条的高度表示温度大小。

// 内部轮廓  一个圆叠加一个矩形  圆形静态  矩形底部和圆形一个颜色  顶部是高温颜色
c.beginPath();
c.fillStyle = obj.temColor1; //预定义填充颜色
// 内外圆间隙
var space = obj.R * 0.2;
var r = obj.R - space; //内圆半径
c.arc(0, 0, r, 0, Math.PI * 2); //阳鱼体外圆弧
c.fill(); //渲染填充轮廓

// 温度区间
var t1 = -10;
var t2 = 40;

var y1 = -r * 1.8; // 温度条上面刻度线开始Y坐标
var y2 = -r - L * 1.0;

var temL = y1 + -(y1 - y2) * (obj.t - t1) / (t2 - t1); //温度对应像素高度位置
c.beginPath();


var temWidth = (startX - space) * 2 // 温度条宽度  解析和内外圈间隙保持一致

// 颜色线性渐变  温度条温度不同 颜色渐变效果不同
var grd = c.createLinearGradient(0, -r, 0, -r - L);
grd.addColorStop(0, obj.temColor1);//低温
grd.addColorStop(0.5, '#00FF4D');//中温
grd.addColorStop(1, obj.temColor2);//高温
c.fillStyle = grd;
// 绘制矩形温度调
c.rect(-temWidth / 2, temL, temWidth, -temL);
c.fill(); //渲染填充轮廓

刻度线绘制

通过for循环调用Canvas直线功能批量创建就可以。小刻度线和大刻度线for环代码整体一样,只是细节不同。

//    文字样式、位置设置
c.fillStyle = "#fff"; //文本填充颜色
c.font = "normal 16px 微软雅黑"; //字体样式设置
c.textBaseline = "middle"; //文本与fillText定义的纵坐标
c.textAlign = "center"; //文本居中(以fillText定义的横坐标)

// 分割25分  小刻度线绘制
var num = 25
for (var i = 0; i < num + 1; i++) {
  c.beginPath();
  c.lineWidth = 1; //obj.R / 40
  c.strokeStyle = '#fff';
  c.moveTo(-startX, y1 + (y2 - y1) / num * i);
  c.lineTo(-startX * 1.5, y1 + (y2 - y1) / num * i);
  c.stroke();
}
// 分割5份  大刻度线绘制
var NUM = 5
for (var i = 0; i < NUM + 1; i++) {
  c.beginPath();
  c.lineWidth = 1; //obj.R / 40
  c.strokeStyle = '#fff';
  c.moveTo(-startX, y1 + (y2 - y1) / NUM * i);
  c.lineTo(-startX * 2, y1 + (y2 - y1) / NUM * i);
  c.stroke();

  //大刻度线竖直自动标注
  c.fillText(t1 - (t1 - t2) / NUM * i + "", -startX * 3, y1 + (y2 - y1) / NUM * i);
}

摄氏度符号

摄氏度符号右上角标记。

c.fillText("℃", startX * 2, y2 - r);

温度计Cnavas案例源码

<body style="background: #001133">
  <div style="width: 800px;margin: auto">
    <!--定义画布  background: rgba(255,255,255,0.17)-->
    <canvas id="canvas" width="100px" height="480px" style="background: rgba(255,255,255,0.09)">
    </canvas>
  </div>
  <script type="text/javascript">
  var temObj = {
    R: 30, // 参数化尺寸基准值  温度计底部圆圈半径
    temColor1: "#0080FF", //低温
    temColor2: "#FF3D3D", //高温
    borderColor: "#CCE6FF", //边框颜色  CCE6FF  0088cc
    lineColor: "#ffffff", //刻度颜色
    t: 40, //当前温度  可以输入温度  -10   0  40可以完全正常显示  能够与刻度线对齐
  }
    thermograph(temObj)

    function thermograph(obj) {
      //    方法一
      let canvas = document.getElementById('canvas');
      c = canvas.getContext('2d');
      //整体调整温度计相对canvas画布的位置
      c.translate(58, 430);
      //    预定义全部线条样式
      c.lineWidth = obj.R / 10;
      c.strokeStyle = obj.borderColor;
      c.beginPath();
      // 起始角度
      var startAngle = -Math.PI / 3;
      var endAngle = Math.PI - startAngle; //与开始角度y轴轴对称角度
      //温度计外轮廓底部圆弧
      c.arc(0, 0, obj.R, startAngle, endAngle, false);
      c.stroke(); //渲染填充轮廓
      // 圆弧起点
      var startX = obj.R * Math.cos(startAngle)
      var startY = obj.R * Math.sin(startAngle)
      // 温度计整体高度
      var L = obj.R * 12
      //温度计外轮廓右侧直线
      c.moveTo(startX, startY);
      c.lineTo(startX, startY - L);
      c.stroke();
      //温度计外轮廓左侧直线
      c.moveTo(-startX, startY);
      c.lineTo(-startX, startY - L);
      c.stroke();
      c.beginPath();
      //温度计外轮廓顶部圆弧
      var startAngle = -Math.PI / 3;
      var endAngle = Math.PI - startAngle; //与开始角度y轴轴对称角度
      //温度计外轮廓底部圆弧
      c.arc(0, startY - L, startX, 0, Math.PI, true);
      c.stroke(); //渲染填充轮廓


      // 内部轮廓  一个圆叠加一个矩形  圆形静态  矩形底部和圆形一个颜色  顶部是高温颜色
      c.beginPath();
      c.fillStyle = obj.temColor1; //预定义填充颜色
      // 内外圆间隙
      var space = obj.R * 0.2;
      var r = obj.R - space; //内圆半径
      c.arc(0, 0, r, 0, Math.PI * 2); //阳鱼体外圆弧
      c.fill(); //渲染填充轮廓



      // 温度区间
      var t1 = -10;
      var t2 = 40;

      var y1 = -r * 1.8; // 温度条上面刻度线开始Y坐标
      var y2 = -r - L * 1.0;

      var temL = y1 + -(y1 - y2) * (obj.t - t1) / (t2 - t1); //温度对应像素高度位置
      c.beginPath();


      var temWidth = (startX - space) * 2 // 温度条宽度  解析和内外圈间隙保持一致

      // 颜色线性渐变  温度条温度不同 颜色渐变效果不同
      var grd = c.createLinearGradient(0, -r, 0, -r - L);
      grd.addColorStop(0, obj.temColor1);
      // grd.addColorStop(0, '#0000ff');
      // grd.addColorStop(0.5, '#00ff00');
      grd.addColorStop(0.5, '#00FF4D');

      // grd.addColorStop(1, '#ff0000');
      grd.addColorStop(1, obj.temColor2);
      c.fillStyle = grd;

      c.rect(-temWidth / 2, temL, temWidth, -temL);
      c.fill(); //渲染填充轮廓


      // 刻度线批量创建



      //    文字样式、位置设置
      c.fillStyle = "#fff"; //文本填充颜色
      c.font = "normal 16px 微软雅黑"; //字体样式设置
      c.textBaseline = "middle"; //文本与fillText定义的纵坐标
      c.textAlign = "center"; //文本居中(以fillText定义的横坐标)


      // 分割25分
      var num = 25
      for (var i = 0; i < num + 1; i++) {
        c.beginPath();
        c.lineWidth = 1; //obj.R / 40
        c.strokeStyle = '#fff';
        c.moveTo(-startX, y1 + (y2 - y1) / num * i);
        c.lineTo(-startX * 1.5, y1 + (y2 - y1) / num * i);
        c.stroke();
      }
      // 分割5份
      var NUM = 5
      for (var i = 0; i < NUM + 1; i++) {
        c.beginPath();
        c.lineWidth = 1; //obj.R / 40
        c.strokeStyle = '#fff';
        c.moveTo(-startX, y1 + (y2 - y1) / NUM * i);
        c.lineTo(-startX * 2, y1 + (y2 - y1) / NUM * i);
        c.stroke();
        c.fillText(t1 - (t1 - t2) / NUM * i + "", -startX * 3, y1 + (y2 - y1) / NUM * i);
      }
      // c.beginPath();
      c.fillText("℃", startX * 2, y2 - r);
    }
  </script>
</body>