您的位置:首页 > Web前端 > HTML5

H5-Canvas-Clock-时钟

2017-03-28 08:24 363 查看

Canvas 示例:时钟

先来看下最终效果图:



星期天趁空隙实现了个简单的一个时钟,下面回顾下其实现原理,和遇到的问题,对 H5 这块还是个菜鸟,只有不断通过练习去熟悉了。

设计

凡是从构思开始,而不是盲目的去实现代码。

设计图:



从上图中该时钟分为几个部分

画布,背景蓝色部分;

第一个圆:时钟边框,最外层 8 个像素的宽边框;

第二个圆:点圆圈,代表着时间划分,每两个点之间代表 12 分钟(60 / 5 = 12);

第三个圆:数字圆,上面依次显示
[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2];
数字,代表小时数;

三个线条,分别是:时针(
r * 0.5
),分针(
r * 0.75
),秒针(
r * 0.85
);

实现

根据上面的设计图和部位划分,来逐步实现 UI。

时钟模块文件:
clock.js


// clock.js

function Clock(canvas) {
this.canvas     = canvas;
this.width      = this.canvas.offsetWidth;
this.height     = this.canvas.offsetHeight;
this.rem        = this.width / 200;
this.ctx        = this.canvas.getContext('2d');
this.r          = this.width / 2;
this.digits     = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2];
this.dotCount   = 60;
this.cellRad    = 2 * Math.PI / this.digits.length;

}


上面定义了时钟构造函数,参数是一个画布对象,成员包含

this.canvas
:缓存画布对象;

this.width
:画布的实际宽度,这里选择让其变成成员,原因是后面会使用到画布宽高,这里创建时进行缓存,避免后面使用时才去计算;

this.height
:画布实际高度,同上;

this.rem
:相对画布的比例,参考值:
200px
,这个成员可以应对画布的大小缩放情况,使其他元素相应的做出响应;

this.ctx
:画布上下文;

this.r
:外圆的半径;

this.digits
:小时数字数组,用来显示在时钟表盘上的数字;

this.dotCount
:定义时钟表盘上的点的数量;

this.cellRad
:小时与小时数字之间的弧度,这里其实就是
30 度
角的弧度;

原型函数列表:(事先定义好可能需要的函数)

Clock.prototype._context
:获取画布上下文,实际上就是得到
this.ctx
成员;

Clock.prototype.drawBg
:绘制背景,也就是最外圈的时钟的边框圆;

Clock.prototype.drawDigits
:绘制数字,根据
单位角度 * 小时数字索引
,将数字绘制到相应的位置上,这里需要注意的是画布的起始位置默认是水平向右的位置开始,也就是 3 点钟方向;

Clock.prototype.drawDot
:绘制点,分割成 60 个点,同样是根据每两个点之间的弧度来实现;

Clock.prototype.drawHourHand
:绘制小时时针线;

Clock.prototype.drawMinuteHand
:绘制分钟时针线;

Clock.prototype.drawSecondHand
:绘制秒钟时针线;

Clock.prototype._drawHand
:绘制时针线的统一函数,因为不管是小时,分钟,秒钟也好,最终都是画线条,因此有其共同点,不同点在于线的粗细,长短,和弧度,因此可统一函数接口,将不同点作为参数传入;

Clock.prototype.drawCenterDot
:绘制三个时针线的中心空心点,让三条线在跳动的时候感觉像是被固定要中心点一样;

Clock.prototype.draw
:对外的绘制接口;

Clock.prototype._update
:将所有绘制接口放到这里面,然后每一秒调用一次去更新当前时间刷新 UI;

Clock.prototype.start
:启动时钟计时;

所有准备工作都OK了,下面就可以进入具体的功能实现部分了。(事先设计好 API 是个很不错的编程思路

时钟边框

时针边框的背景就是个具有边框的空心圆,原理是很简单的

// clock.js

Clock.prototype.drawBg = function () {

var ctx         = this._context(), // 获取画布上下文
lineWidth   = 8,    // 指定线的宽度

// 这个半径需要考虑到边框
r           = this.r - lineWidth * this.rem / 2;

// 重置画布原点至画布中心点,因为后面的绘制都需要以中心点为圆点
// 为了方便起见,这里直接将中心点作为原点是个不错的选择
ctx.translate(this.r, this.r);

// 开始路径
ctx.beginPath();

// 指定线宽
ctx.lineWidth = lineWidth;

// 参数分别对应:圆点(x:0, y:0),半径:r,起始弧度:0,结束弧度:2π
ctx.arc(0, 0, r, 0, 2 * Math.PI);

// 结束路径,这里需要提醒的一点,最好先成对把 beginPath 和 closePah 先好
// 避免遗漏关闭路径
ctx.closePath();

// 绘制边框,如果填充则需要使用 ctx.fill();
ctx.stroke();
};


这样时钟边框圆就绘制完成了,如下图:



点圆绘制

第二个圆:点圆的绘制,整个时钟会被分割成 60 份,包含 60 个点,也就是说两个小时数之间会有5个分割弧度如设计图中右下角部分的绿色线条;

这个点圆的绘制原理是:根据弧度来计算每个点中心点的的具体坐标(x, y),如下图



圆分割成60个圆弧后,每个圆弧的弧度:

rad = 2 * Math.PI / 60;


比如:

第一个点:12点上 (0,r)

第二个点:
(x = r * cos(rad), y = r * sin(rad));


第三个点:
(x = r * cos(2 * rad), y = r * sin(2 * rad));




第N个点:
(x = r * cos((n - 1) * rad), y = r * sin((n - 1) * rad));


根据上面的结果,那么我们代码就很简单了

// clock.js

Clock.prototype.drawDot = function () {

var ctx     = this._context(),
delta   = 18,
r       = this.r - delta * this.rem,
dotRad  = 2 * Math.PI / this.dotCount,
x, y, i;

// 遍历所有的点,获取每个点的坐标(x,y),以该坐标为圆点绘制
for (i = 0; i < this.dotCount; i++ ) {

// 计算当前点弧度
rad = dotRad * i;

// 根据弧度得到点坐标
x = r * Math.cos(rad);
y = r * Math.sin(rad);

ctx.beginPath();
// 以 (x, y)为圆点绘制,半径为 1 的小圆点,采用填充方式
// 这里 * this.rem,是应对画布缩放的时候点圆的大小等比例缩放
ctx.arc(x, y, 1 * this.rem, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = 'gray';
if ( i % 5 === 0 ) {
// 小时数上的点,以黑色强调显示
ctx.fillStyle = 'black';
}
ctx.fill();
}
};


这样就完成了外圈圆,和中间点圆的绘制,效果图如下:



点圆的绘制,重点在于每个点的左边的位置,根据被分割的单元弧度和点的索引即可计算出该点的弧度;

数字圆的绘制

数字圆的绘制和点圆的绘制原理是一样的,只是被分割的单元弧度不一样而已,另外需要注意的点是,数字左边上的文字布局问题;

先来看下代码:

// clock.js

Clock.prototype.drawDigits = function () {

var me      = this,
ctx     = this._context(),
delta   = 30,
r       = this.r - delta * this.rem,
rad     = 0,
x, y;

this.digits.forEach(function (digit, index) {

rad = me.cellRad * index;

x = r * Math.sin(rad);
y = r * Math.cos(rad);

ctx.font = '18px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(digit + '', x, y);
});

return this;
}




这里绘制方式很简单,直接定位到点(x, y),使用
fillText
就会在该点处直接填充内容,主要看下这里两个属性:

ctx.textAlign
ctx.textBaseLine


ctx.textBaseLine
:文本基线对齐

这个属性可以指定指定点上的文本基线对齐方式,默认:普通的字母基线(
alphabetic


w3school 上的各种取值的基线对齐图:



可取值列表:

描述
alphabetic普通的字母基线
topem 方框的顶端
hanging悬挂基线,单词最高字母的顶部紧贴基线方式
middleem 方框的正中
ideographic表意基线
bottomem 方框底部
图解:



除了
ideographic
这个没太理解之外,其他的都还好理解,而我们这里用到的就是
middle
根据
em
方框的正中对齐;

ctx.textAlign
: 文本位置

这个相对就比较好理解了,看图



时针绘制(小时,分钟,秒钟)

把小时,分钟,秒钟时针放一起,因为它们三个的绘制函数是一样的,只是需要控制其不通的角度和样式

/**
* 绘制线条(时针,分针,秒针线)
* @param  {Number} rad    弧度,当前时间对应的弧度
* @param  {Number} length 时间针的长度
* @param  {String} style  时间针的填充样式
* @param  {Number} width  时间针的宽度
* @return {[type]}        [description]
*/
Clock.prototype._drawHand = function (rad, length, style, width) {
var ctx = this._context();

ctx.save();
ctx.beginPath();

// 这个负责根据弧度转动指针,也是根据当前时间实时更新时针的关键
ctx.rotate(rad);

// 线条两端样式,可取值:butt|round|square
ctx.lineCap = 'round';
ctx.lineWidth = width;
ctx.strokeStyle = style;

// 这里使用了个技巧,让起点位置往后突出了 10 个像素
ctx.moveTo(0, 10 * this.rem);
ctx.lineTo(0, -length);
ctx.closePath();
ctx.stroke();
ctx.restore();

return this;
}


接下来根据不同指针的弧度绘制线条

时针

Clock.prototype.drawHourHand = function ( hour, minute ) {

var rad, cellRad;

cellRad = 2 * Math.PI / this.digits.length;

// 小时数加上分钟数的弧度,如果不设置分钟数弧度,
// 时针只会指向特定的小时数位置
rad =  cellRad * hour + minute / 60 * cellRad;

this._drawHand(rad, this.r * 0.5, 'black', 5);

return this;
};


分针

Clock.prototype.drawMinuteHand = function ( minute ) {

var rad = minute * (2 * Math.PI / this.dotCount);

this._drawHand(rad, this.r * 0.6, 'black', 5);

return this;
};


秒针

Clock.prototype.drawSecondHand = function ( second ) {

var rad = second * (2 * Math.PI / this.dotCount);

this._drawHand(rad, this.r * 0.78, 'gray', 2);

return this;
};


加上中间圆点

Clock.prototype.drawCenterDot = function () {

var ctx = this._context(),
x, y;

ctx.beginPath();
ctx.arc(0, 0, 2 * this.rem, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = 'white';
ctx.fill();

return this;
}


最终效果:



到此时钟的 UI 界面算是完成了。

指针实时更新

动起来,这里就需要用到计时器,去每个一秒获取当前时间的时分秒,去刷新三个指针的位置

更新画布函数:

Clock.prototype._update = function (h, m, s) {

this.ctx.clearRect(0, 0, this.width, this.height);
this.drawBg();
this.drawDigits();
this.drawDot();
this.drawHourHand(h, m);
this.drawMinuteHand(m);
this.drawSecondHand(s);
this.drawCenterDot();
this.ctx.restore();

return this;
}


注意点:

return this;
之前的
this.ctx.restore();


还原状态,还记得绘制背景的
this.drawBg();
里面我们将绘制的起点位置使用
ctx.translate(this.r, this.r);
重新设置到了
(this.r, this.r)


如果这里不进行还原,那么下一次重绘会在
(this.r, this.r)
的基础上再去重设起点,会导致刷新的时钟不断沿右下 45 度角上不断延伸;

如果我们这里在重绘整个时钟之前进行还原,那么画布起点就会回到
(0, 0)
也就画布左上角位置,这样才能正确进入下一个时钟循环进行绘制

this.ctx.clearRect(0, 0, this.width, this.height);
清除画布

这一句的作用在于绘制下一个时钟之前,清除画布上的指定矩形区内的所有内容,如果不清除,会发生不断重叠的现象,如下图:



通过
this.ctx.clearRect
通过这个清除函数,可以将画布内容清空,方便绘制下一个时钟,其实就是清空画布,重新绘制整个时钟

启动更新:

Clock.prototype.start = function () {

var date = new Date(),
hour, minute, second,
me = this;

// 得到当前时间的时分秒
hour    = date.getHours();
minute  = date.getMinutes();
second  = date.getSeconds();

// 更新画布
this._update(hour, minute, second);

// 每个一秒刷新一次,这里用到了 requestAnimationFrame 动画帧请求函数
// 对于这个函数还没搞透,为啥就比 setTimeout 更准确的问题
requestAnimationFrame(function () {
me.start();
}, 1000);
};


最终效果动态图



总结

一个小时钟实现,原理其实很简单,主要实现步骤

绘制外圈

绘制点圈

绘制数字

绘制三个指针

最后设置定时器更新指针

涉及的属性:

属性名描述
lineWidth线条宽度
lineCap线条两端样式,
butt
,
round
,
square
font字体
textAlign文本水平对齐方式,
right
,
left
,
end
,
start
,
center
textBaseLine基线对齐方式,
alphabetic
,
top
,
hanging
,
middle
,
ideographic
,
bottom
使用到的函数:

绘图函数:

ctx.arc(x, y, r, startRad, endRad);
:绘制圆或圆弧;

ctx.moveTo(x, y);
ctc.lineTo(x, y);
: 绘制线条;

状态函数:

ctx.save();


ctx.restore();


清空画布函数:

ctx.clearRect(x, y, w, h);


其他函数:

ctx.rotate(rad);
: 旋转角度;

ctx.fill/fillStyle
: 填充和填充样式;

ctx.stroke/strokeStyle
:描边和描边样式;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  canvas h5 javascript