环形温度调节实现

同事要做个物联网小程序,可以通过小程序控制家里空调的温度设置(具体怎么实现以后研究)。温度控制界面上一个大大的环形,需求是拨动这个环形上的拖柄,可控制环形色条。

先来预设一些数值

  • 暂定画布宽高为绝对值(500px * 500px)(实际情况可能需要做成自适应的尺寸)
  • 轨道圆形的圆心为(250, 250),半径分别为100px、120px
  • 上面两个条件之后轨道上的小圆型(滑柄)半径为20, 圆心坐标为(360,250)

这几个数值下面要用。

模板如下:

1
2
3
4
<view style="">
<canvas @touchstart='touchStart' @touchmove='touchMove' style="width: 500px; height: 500px;background: #333333;"
canvas-id="secondCanvas"></canvas>
</view>

data:

1
2
3
4
5
6
7
objCanvas: null,  // canvas画布对象
handleArc: { // 轨道上小圆行初始值
r: 20,
x: 360,
y: 250,
angle: 0
},

1. 设计思路

  • 这样的需求只能选用canvas画布,div做出的环形无法控制颜色任意填充面积;
  • 画布上的环形需要一个‘‘运动轨道’’ 和 一个可以用来拖拽的小圆(下文称为滑柄),小圆要在“轨道”上运动;
  • 拖动的同时要限制小圆,不能脱离轨道,轨道填充颜色要和小圆一致;

2.我们先来完成初始状态绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

this.objCanvas = wx.createCanvasContext('secondCanvas');
var context = this.objCanvas;

context.setStrokeStyle("#00ff00")

// 先画出轨道 内轨圆形轮廓
context.beginPath();
context.arc(250, 250, 100, 0, 2 * Math.PI, true)
context.stroke()

// 画出轨道 外轨圆形轮廓
context.beginPath();
context.arc(250, 250, 120, 0, 2 * Math.PI, true)
context.stroke()

// 绘制轨道填充圆
context.beginPath();
context.moveTo(250,250);
context.arc(250, 250, 120, this.handleArc.angle * Math.PI /180, 0 * Math.PI /180, true)
context.closePath();
context.fillStyle='#f15a4a';
context.fill();

// 填补轨道内圆形颜色,使其展示为空心圆
context.beginPath();
context.moveTo(250,250);
context.arc(250, 250, 100, 0, 2 * Math.PI, true)
context.closePath();
context.fillStyle='#333333';
context.fill();

// 绘制滑块轨道上圆形滑块
context.beginPath();
context.moveTo(200,200);
context.arc(this.handleArc.x, this.handleArc.y, this.handleArc.r, 0,Math.PI*2,false);
context.fillStyle='#f15a4a';
context.fill();

// 绘制
context.draw()

大概样子如下:

初始

3. 怎样让他能拖动呢?

(1)首先来复习一下三角函数
三角函数

复习几个js的Math对象方法:

方法 描述
abs(x) 返回数的绝对值。
acos(x) 返回数的反余弦值。
asin(x) 返回数的反正弦值。
atan(x) 以介于 -PI/2 与 PI/2 弧度之间的数值来返回 x 的反正切值。
atan2(y,x) 返回从 x 轴到点 (x,y) 的角度(介于 -PI/2 与 PI/2 弧度之间)。
ceil(x) 对数进行上舍入。
cos(x) 返回数的余弦。
exp(x) 返回 e 的指数。
floor(x) 对数进行下舍入。
log(x) 返回数的自然对数(底为e)。
max(x,y) 返回 x 和 y 中的最高值。
min(x,y) 返回 x 和 y 中的最低值。
pow(x,y) 返回 x 的 y 次幂。
random() 返回 0 ~ 1 之间的随机数。
round(x) 把数四舍五入为最接近的整数。
sin(x) 返回数的正弦。
sqrt(x) 返回数的平方根。
tan(x) 返回角的正切。
toSource() 返回该对象的源代码。
valueOf() 返回 Math 对象的原始值。

当然不会都用上,顺便复习其他的

(2)再来理一下思路
  • canvas画布上的原始坐标轴以左上角为(0, 0)原点,原点为基础,右方位x轴正方向,下方为y轴正方向;
  • 手指(或鼠标)在进入canvas画布就开始检测是否可以触发滑柄滑动;
  • 根据手指的位置需要转换为滑柄在轨道上的准确位置;

手指在画布移动事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
touchMove(e) {
// 手指移入进画布后,获取坐标
var cli = {};
cli.x = e.touches[0].x;
cli.y = e.touches[0].y;

// 矫正为滑柄在轨道坐标
var b = this.checkDirc(cli);

this.handleArc.x = b.x
this.handleArc.y = b.y
this.handleArc.angle = b.angle

// 重新绘制画布
this.reDrawCanvas();
},

主要是如何获取滑柄在轨道上的准确位置

利用相似三角形定律和几个三角函数如下:

你看这个图他又丑又草,凑合着看版本

解题过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 判断是左还是右
checkDirc: function(point) {

// 获取小圆心在轨道圆上的坐标

// 以手指和轨道圆心为斜边的直角三角形和滑柄圆心和轨道圆心为斜边的直角三角形是相似三角形
// 手指斜边
var pc = Math.sqrt(Math.pow((point.x - 250), 2) + Math.pow((point.y - 250), 2));
var ph = point.y - 250;
var pa = point.x - 250;

// 滑柄在运动轨迹圆半径
var c = 110;
var h = Math.abs((ph * c) / pc);
var a = Math.abs(Math.sqrt(Math.pow(c, 2) - Math.pow(h, 2)))

// 计算出夹角度数
var angle = Math.asin(h / c) * 180 / Math.PI;
var handleArcPoint = {};

// 手指可能在(250, 250)的四个方位,根据不同的方向,做相应计算
// 判断方向,获取小圆坐标
if (point.x > 250 && point.y > 250) {
handleArcPoint.x = 250 + a;
handleArcPoint.y = 250 + h;
angle = angle;
} else if (point.x > 250 && point.y < 250) {
handleArcPoint.x = 250 + a;
handleArcPoint.y = 250 - h;
angle = 90 - angle + 270;

} else if (point.x < 250 && point.y > 250) {
handleArcPoint.x = 250 - a;
handleArcPoint.y = 250 + h;
angle = 90 - angle + 90;
} else if (point.x < 250 && point.y < 250) {
handleArcPoint.x = 250 - a;
handleArcPoint.y = 250 - h;
angle = angle + 180;
}

if (this.handleArc.angle > 270) {
handleArcPoint.x = 250;
handleArcPoint.y = 250 - 110;
angle = 270;
}

handleArcPoint.angle = angle;
return handleArcPoint;
}

基本上可以了,然后就是重新绘制画布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

reDrawCanvas: function() {
var context = this.objCanvas;

// 清除画布颜色(实际就是覆盖颜色)
context.clearRect(0, 0, 500, 500)

context.setStrokeStyle("#00ff00")
context.beginPath();
context.arc(250, 250, 100, 0, 2 * Math.PI, true)
context.stroke()

context.beginPath();
context.arc(250, 250, 120, 0, 2 * Math.PI, true)
context.stroke()

context.beginPath();
context.moveTo(250, 250);
context.arc(250, 250, 120, this.handleArc.angle * Math.PI / 180, 0 * Math.PI / 180, true)
context.closePath();
context.fillStyle = '#f15a4a';
context.fill();

context.beginPath();
context.moveTo(250, 250);
context.arc(250, 250, 100, 0, 2 * Math.PI, true)
context.closePath();
context.fillStyle = '#333333';
context.fill();

context.beginPath();
context.moveTo(200, 200);
context.arc(this.handleArc.x, this.handleArc.y, this.handleArc.r, 0, Math.PI * 2, false); // 绘制滑块
context.fillStyle = '#f15a4a';
context.fill();

context.draw()
}

最终效果如下:
结果

4. 注意

小程序有自己的长度单位rpx,但是canvas是个例外,canvas只认px,所以,实际使用在小程序时,需要再处理一下单位,最好方案是响应式的单位(原理和rem类似)