前言

前段时间公司有个数据可视化的项目,要求就是动感炫酷,恰好我想到有个降雨量可以用球型加载的效果,于是便自己动手写了个水球插件,插件可以在我的github上下载。

样式预览

实现方法

当时看了echarts上的扩展水球,我想水波浪可以用正弦函数实现,通过移动正弦函数实现动态效果,有点像示波器里的电信号图表。

画一段sin()函数

正弦函数的特性大家应该都知道,当函数为sin(x)时,值域为[-1, 1],周期为2π。
Alt text
由上图可知,要在一段轴长内画正弦曲线,通过循环遍历,将函数的每个点串联为曲线。
接下来就开始写吧:

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
var canvas=document.getElementById('myCanvas');
var ctx=canvas.getContext('2d');
//确定Sin 曲线属性
var sX = 0;
var sY = canvas.height / 2;
var axisLength = canvas.width; //轴长
var waveWidth = 0.02 ; //波浪宽度,数越小越宽
var waveHeight = 30; //波浪高度,数越大越高
ctx.lineWidth = 1;//线宽
//画sin 曲线函数
var drawSin = function(){
ctx.save();
var points=[]; //用于存放Sin曲线的坐标点
ctx.beginPath();
for(var x = sX; x < sX + axisLength; x += 20 /axisLength){
var y = -Math.sin((sX + x) * waveWidth);
points.push([x, sY + y * waveHeight]);
ctx.lineTo(x, sY + y * waveHeight);
}
//封闭路径
ctx.lineTo(axisLength, canvas.height);//右下角终点
ctx.lineTo(sX, canvas.height);//左下角终点
ctx.lineTo(points[0][0],points[0][1]);//sin()曲线起始点
ctx.stroke()
ctx.restore();
};
drawSin()

Alt text
这样一段sin()曲线就画好了。

画一个圆容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var r = canvas.height / 2; //圆心
var cR = r - 16 * 1; //圆半径
var drawCircle = function()
ctx.beginPath();
//这里的cR+8里的8是外圈和内圈的距离
ctx.arc(r, r, cR+8, 0, 2 * Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.arc(r, r, cR, 0, 2 * Math.PI);
ctx.clip()
};
drawCircle();
//为sin()曲线加一个填充颜色
/*
*修改drawSin()函数
*ctx.fillStyle = '#444';ctx.fill()
*/

Alt text

让曲线动起来

原理是不断改变sin()函数的原点,重绘sin()曲线。
加入速度控制(函数x轴原点每次的偏移量)

1
2
3
4
5
6
7
8
9
10
11
12
var speed = 0.04; //波浪速度,数越大速度越快
var xOffset = 0; //波浪x偏移量,在drawSin()函数里增加这个参数drawSin(xOffset),并修改
var y = -Math.sin((sX + x) * waveWidth+xOffset);
var render = function(){
ctx.clearRect(0, 0, mW, mH);
drawSin(xOffset);
xOffset += speed; //形成动态效果
window.requestAnimationFrame(render);
};
render()

Alt text
这里有个window.requestAnimationFrame()API。这个方法告诉浏览器您希望执行动画并请求浏览器调用指定的函数在下一次重绘之前更新动画。具体详解大家可以看张鑫旭大神的CSS3动画那么强,requestAnimationFrame还有毛线用?

这样我要的效果基本就实现了。

封装插件

接下来我们便将这个效果封装为插件,便于多次使用。
创建闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
(function(window,factory){
if(typeof define === "function" && define.amd){
//AMD
define(factory);
}else if(typeof module === "object" && module.exports){
//CMD
module.exports = factory();
}else{
//window
window.WaterPolo = factory();
}
}(typeof window !== "undefined" ? window : this,function(selector,userOptions){
}))

采用window作为参数传入,将功能函数挂载到window上。接下来写功能函数:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
var WaterPolo=function(selector,userOption
'user strict';
userOptions=userOptions||{};
//基本参数设置
var options={
//容器距边缘的距离
wrapW:3,
//canvas属性
cW:300,
cH:300,
lineWidth : 2,
//液面位置 百分比表示
baseY: 20,
//页面起始位置
nowRange: 0,
//线条颜色
lineColor:'rgb(176,204,53)',
//上层颜色
oneColor:'rgba(176,204,53,.6)',
//下层颜色
twoColor:'rgba(176,204,53,.4)',
//上层波浪宽度,数越小越宽
oneWaveWidth:0.06,
//下层波浪宽度
twoWaveWidth:0.06,
//上层波浪高度,数越大越高
oneWaveHeight:4,
//下层波浪高度
twoWaveHeight:4,
//上层波浪x轴偏移量
oneOffsetX:10,
//下层波浪x轴偏移量
twoOffsetX:20,
//波浪滚动速度,数越大越快
speed:0.2
};
var canvas = null,
ctx = null,
W = null,
H = null;
//监听对象属性变化
Object.defineProperty(this, 'options', {
get: function() {
return options;
},
set: function(value) {
mergeOption(value,options);
}
});
//参数混合相当于$.extend([old],[new])
var mergeOption=function(userOptions,options){
Object.keys(userOptions).forEach(function(key){
options[key]=userOptions[key];
})
//生成液面
var makeLiquaid=function(ctx,xOffset,waveWidth,waveHeight,color){
ctx.save();
var points = [];//用于存放绘制Sin曲线的点
ctx.beginPath();
//在x轴上取点
for (var x = 0; x < options.cW; x += 20 / options.cW) {
//此处坐标(x,y)的取点,依靠公式 “振幅高*sin(x*振幅宽 + 振幅偏移量)”
var y = -Math.sin(x * waveWidth + xOffse
//液面高度百分比改变
var dY = options.cH * (1 - options.nowRange / 10
points.push([x, dY + y * waveHeight]);
ctx.lineTo(x, dY + y * waveHeight);
}
//封闭路径
ctx.lineTo(options.cW, options.cH);
ctx.lineTo(0, options.cH);
ctx.lineTo(points[0][0], points[0][1]);
ctx.fillStyle = color;
ctx.fill();
ctx.restore();
//初始
var init=function(){
mergeOption(userOptions,option
canvas=document.getElementById(selector);
ctx=canvas.getContext('2d
canvas.width=options.cW;
canvas.height=options.
ctx.lineWidth=options.lineWid
//圆属性
var r = options.cH / 2; //圆心
var cR = r - 6; //圆半径 决定圆的大小
var drawCircle = function(ctx){
ctx.beginPath();
ctx.strokeStyle = options.lineColor;
ctx.arc(r, r, cR+options.wrapW, 0, 2 * Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.arc(r, r, cR, 0, 2 * Math.PI);
ctx.clip
};
drawCircle(ctx);/
(function drawFrame
window.requestAnimationFrame(drawFram
ctx.clearRect(0, 0, options.cW, options.cH);
//高度改变动
if (options.nowRange <= options.baseY) {
var tmp = 1;
options.nowRange += tmp;
if (options.nowRange > options.baseY) {
var tmp = 1;
options.nowRange -= tmp;
}
makeLiquctx,options.oneOffsetX,options.oneWaveWidth,options.oneWaveHeight,options.oneColor);
makeLiquctx,options.twoOffsetX,options.twoWaveWidth,options.twoWaveHeight,options.twoColo
options.oneOffsetX+=options.speed;
options.twoOffsetX+=options.spe
}());
};
init();
};
return WaterPolo;

使用插件

1
2
3
4
5
6
7
8
9
10
11
12
//var a=new WaterPolo('canvas');
//你对默认属性不喜欢的,也可以传入一些新属性
var newOptions={
cW:130,
cH:130,
baseY:50,
nowRange:0
};
var a=new WaterPolo('canvas',newOptions);
//更新属性
newOptions.baseY=40;
a.options=newOptions;

封装插件时用到了ES5的Object.defineProperty()这个方法,这个也是vue.js关于视图和数据动态变化的原理,所以理解Object.defineProperty()是必须的,在我的解析Object.defineProperty有些讲解。