到第五讲时,高颜值的时钟还不会走时,这是因为缺乏驱动。驱动将通过 requestAnimationFrame API接口来实现。上一讲里,我们已经将画时钟的所有细节集中放到了一个函数里,函数名叫 render(),我们把 requestAnimationFrame(render) 放入该函数内,函数自身只要从外部执行一次就会得到反复调用,直到手工停止它的执行或页面被关闭。代码结构如下:
//函数 :绘制时钟部件集合
let render = () => {
//这里是绘制时钟具体部件的代码集群
requestAnimationFrame(render); //请求关键帧动画从函数内部调用函数
};
requestAnimationFrame 其实也是一个定时器,它依据屏幕刷新率作为自己的执行频率,就是说,一秒钟里面它至少执行60次上下的任务,这取决于显示器的刷新频率的高低,频率越高,它执行任务的次数越多。
不过问题是,假如我们现在就将 requestAnimationFrame(render); 加入到函数 render() 中,情况和第五节的最终效果没什么两样,时钟还是没能走时。这又是为什么呢?请看三根指针的绘制代码:
draw_rect(0, -3, 90, 6, 0, 'lightgreen'); /* 时针 */
draw_rect(0, -2, 100, 4, 270 * Math.PI/180, 'lightgreen'); /* 分针 */
draw_rect(0, -1, 120, 2, 240 * Math.PI/180, 'lightgreen'); /* 秒针 */
以秒针为例,它通过绘制矩形函数 draw_rect() 绘制出来,函数将画布坐标系移到了画布的中心,指针总是从圆心区域画起,向三点钟方向画一定宽高的矩形,requestAnimationFrame(render) 绘制秒钟就这么重复着画。要转动指针,实际上是转动画布坐标系,函数提供了一个 rad 参数,我们绘制的秒针传值是 240 * Math.PI/180 弧度,秒针定格在 11 点钟方向。这里,240是十一点钟方向的角度值,改变这个角度值,秒针的指向就会发生变化。所以,我们首先需要知道当下的秒数是多少,然后根据算法计算出这个秒数是多少度就可以将它替代 240 令秒针每一秒钟都指向正确的方向,秒钟就动起来了。
获得当下时间信息,需要用到JS的 Date 对象:通过声明一个变量将其实例化,就能从实例化变量中获得所需的年月日时分秒等具体时间信息:
// 将 Date 对象实例化为 now
let now = new Date();
// 从 now 实例化对象中获取时间信息
let year = now.getFullYear(); //年
let month = now.getMonth() + 1; //月 加1的原因:getMonth() 返回 0~11 的数值
let date = now.getDate(); //日
let day = now.getDay(); // 星期几(返回 0~6 的数值)
let hour = now.getHour(); //小时
let minute = now.getMinute(); //分钟
let second = now.getSecond(); //秒钟
let msec = now.getMilliseconds(); //毫秒
通过上面的时间信息获取方法,我们用 now.getSecond() 就能拿到当下的秒数。那么,秒数和角度又有什么关系呢?秒针指向刻度,而时钟上的每一个刻度向圆心的延长线都会和指向三点钟方向的X轴形成一定角度的夹角。理论上,每一秒钟的行进距离就是一个刻度的距离,亦即,它走的角度值为 6 度,那么,假设现在的秒数是 sec 秒,则角度为 sec * 6,为了精准,还需要考虑毫秒数,假设现在的毫秒数为 msec,依据毫秒和秒的关系,其所在度数为 msec * 0.36 / 1000,这个要加到秒钟的偏移度数中。我们来看代码:
/* 获取时间信息 */
let now = new Date();
//获取秒数和毫秒数
let sec = now.getSeconds(),
msec = now.getMilliseconds();
//转换秒数毫秒数为角度
let sDeg = sec * 6 + (msec * 0.36 / 1000);
//绘制秒针
draw_rect(0, -1, 120, 2, (sDeg - 90) * Math.PI/180, 'lightgreen');
其他指针同理获得角度,然后将角度转换成弧度作为传参调用 draw_rect 依次绘制出来。这些,统统加入到 render() 函数中来,连同时钟的其他所有元素的具体绘制封装在一起,render() 内部在最后通过 requestAnimationFrame 按屏幕的刷新率去驱动时钟。时钟的最终效果以及全部代码如下,代码中,尤其需要注意对时间信息和时针转动角度的处理,这可是本讲的核心:
<div style="margin-top: 20px; text-align: center;">
<canvas id="canv" width="300" height="300"></canvas>
</div>
<script>
/* 获取画笔 */
let ctx = canv.getContext('2d');
/* 函数 :绘制矩形(指针) */
let draw_rect = (x, y, w, h, rad, color) => {
ctx.save();
ctx.fillStyle = color;
ctx.translate(150,150);
ctx.rotate(rad);
ctx.fillRect(x,y,w,h);
ctx.restore();
};
/* 函数 :绘制圆(环) */
let draw_circle = (x,y,r,lw,color1,color2) => {
ctx.save();
ctx.fillStyle = color1;
ctx.strokeStyle = color2;
ctx.lineWidth = lw;
ctx.beginPath();
ctx.arc(x,y,r,0,Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
};
/* 函数 :绘制文本 */
let draw_text = (txt,x,y,color,fontsize=18,b="bold") => {
ctx.save();
ctx.translate(150,150);
ctx.font = `${b} ${fontsize}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline="middle";
ctx.fillStyle = color;
ctx.fillText(txt,x,y);
ctx.restore();
};
let render = () => {
/* 获取时间 */
let now = new Date();
let hr = now.getHours() > 12 ? now.getHours() - 12 : now.getHours(),
min = now.getMinutes(),
sec = now.getSeconds(),
msec = now.getMilliseconds();
let hDeg = hr * 30 + (min * 6 / 12),
mDeg = min * 6 + (sec * 6 / 60),
sDeg = sec * 6 + (msec * 0.36 / 1000);
ctx.clearRect(0,0,300,300); /* 每一次绘制前都衔擦除画布 */
draw_circle(150,150,140,10,'tan','darkgreen'); /* 钟壳和钟面 */
/* 钟点 */
for(let i = 0; i < 12; i ++) {
let radian = Math.PI/180*(i*30-60);
let x = 115 * Math.cos(radian), y = 115 * Math.sin(radian);
draw_text(i+1, x, y, 'green');
}
/* 刻度 */
for(let i = 0; i < 60; i ++) {
let radian = Math.PI/180*(i*6);
let x = 150 + 130 * Math.cos(radian), y = 150 + 130 * Math.sin(radian);
draw_circle(x,y,1,2,'gray','gray');
}
/* 按一定次序绘制时钟各个元素 :确保指针、指针扣不会被遮挡 */
draw_text(`${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`,0,-50,'white', 15, 'normal'); /* 日期 */
draw_text(`星期${'日一二三四五六'.substr(now.getDay(),1)}`,0,-25,'white', 15, 'normal'); /* 星期 */
draw_text('HUACHAO',0,60,'gray'); /* Logo */
draw_rect(0, -3, 90, 6, (hDeg - 90) * Math.PI/180, 'lightgreen'); /* 时针 */
draw_rect(0, -2, 100, 4, (mDeg - 90) * Math.PI/180, 'lightgreen'); /* 分针 */
draw_rect(0, -1, 120, 2, (sDeg - 90) * Math.PI/180, 'lightgreen'); /* 秒针 */
draw_circle(150,150,6,6,'white','lightgreen'); /* 指针扣 */
requestAnimationFrame(render);
};
render();
</script>
《做一个canvas时钟》的课程至此结束。整体课程可能颇为抽象,尽管我已经极尽所能讲的很通俗了。时钟牵扯到的知识面很广,仅canvas画布的内容就相当不好理解,而且很多朋友此前对此没有什么基础。不过事在人为,能够消化这六个讲义,对提升编程水平和做帖能力等等都会有潜移默化的促进作用,甚至对改变自己的工作、学习和生活态度也会有正面的影响。谢谢大家!