使用canvas画一个时钟
介绍一下如何使用 HTML5 的 canvas 标签和 JavaScript 画一个时钟。
可以点击这里看看效果。
目录:
创建 canvas 标签
<div>
<canvas id="clock" width="200px" height="200px"></canvas>
</div>
值得注意的是,要使用Canvas元素,必须设置其width和height属性,制定绘图区域大小。
获取canvas元素并创建上下文对象
//获取上下文
var ctx = document.querySelector("#clock").getContext('2d');
//获取canvas的宽和高
var width = ctx.canvas.width;
var height = ctx.canvas.height;
//设置时钟的半径
let r = width / 2;
如果我们要在这块画布上绘图,需要取得绘图上下文,通过调用 getContext() 方法并传入上下文名字获取。
getContext(“2d”) 对象是内建的 HTML5 对象,拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。
在这里,我们还获取了 canvas 元素的宽和高,设置时钟的半径为画布宽度的一半。
querySelector() 方法返回文档中匹配指定 CSS 选择器的一个元素(仅仅返回匹配指定选择器的第一个元素)。如果需要返回所有的元素,可以使用 querySelectorAll() 方法替代。
画时钟的整体骨架
function draw(){
//保存画布状态
ctx.save();
//清除画布内容
ctx.clearRect(0, 0, width, height);
//获取当前的时间
var now = new Date();
var hour = now.getHours();
var minute = now.getMinutes();
var second = now.getSeconds();
//画时钟的背景
drawBackground();
//画时针
drawHour(hour,minute);
//画分针
drawMinute(minute);
//画秒针
drawSecond(second);
//画时钟的原点
drawDot();
//还原画布原始状态
ctx.restore();
}
ctx.save() 和 ctx.restore() 是两个相互匹配的方法,他们的作用是保存当前环境的状态和返回之前保存过的路径状态和属性。我们需要知道的是,当我们对画布进行旋转、缩放、平移等操作的时候,其实我们不是单单为某一特定的元素进行操作而是对整个画布进行了操作,那么问题就产生了,每次操作都会使所有在画布上的元素受到影响,所以我们在进行操作之前都需要调用 ctx.save() 来保存画布当前的状态,操作结束之后调用 ctx.restore() 取出之前保存过的状态,这样就可以避免对其他的元素进行影响。
在这里,我们一开始对画布状态实现了保存,当时钟都画完之后,我们再对时钟的初始状态进行还原,这样就不会影响下一秒画时钟时画布的状态了。
其实,用 canvas 本身就是一个静态画面,要实现动画,就是不停地擦除原来画布上的内容然后画上新的内容,我们只需要一秒钟重绘一次就能实现我们的时钟动画了。
ctx.clearRect(x, y, width, height) 方法的作用是在给定的矩形内清除指定的像素,我们在每次重绘时钟之前就使用这个方法来清除画布上的内容然后画上下一秒时钟的内容。
在每次画时钟之前,我们还需要获取当前实时的时间,我们创建了 Date 对象,然后调用它的 getHours()、getMinutes()、getSeconds() 方法获取当前时间的小时数、分钟数、秒数。
然后我们就可以一步步的把时钟画出来了。
创建 Date 实例能用来处理日期和时间。(关于 Date 对象的知识可以看看这里)
画时钟的背景 (点数以及边框)
function drawBackground(){
//定义画布中心为(0,0)
ctx.translate(r, r);
//画边框
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(0, 0, r-ctx.lineWidth/2, 0, 2*Math.PI, false);
ctx.stroke();
//画小时数
var hourNumbers = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2];
ctx.font ='18px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
hourNumbers.forEach(function(number, i){
var rad = 2*Math.PI / 12 * i;
var x = Math.cos(rad) * (r - 30);
var y = Math.sin(rad) * (r - 30);
ctx.fillText(number, x, y);
})
//画秒对应的点
for(var i = 0; i<60;i++){
var rad = 2*Math.PI/60 *i;
var x = Math.cos(rad) * (r - 18);
var y = Math.sin(rad) * (r - 18);
ctx.beginPath();
if(i%5 == 0){
ctx.fillStyle = '#000';
}else {
ctx.fillStyle = '#ccc';
}
ctx.arc(x, y, 2, 0, 2*Math.PI, false);
ctx.fill();
}
}
为了方便,我们一开始使用 translate(x, y) 方法把画布的 (0,0)点放在画布的中心位置。需要注意的是画布的坐标系和我们平时的坐标系并不完全一样,x 轴和 y 轴正方向取正数:
接着我们开始画时钟的边框,先设置线宽(lineWidth)使边框看起来粗一些,然后使用arc(x, y, r, startAngle, endAngle, counterclockwise) 方法,把起始角设置为 0,结束角设置为 2*Math.PI 就能很简单的画出一个圆的轨迹,最后调用 stroke() 方法画出轨迹。
注意一下 stroke() 和 fill() 这两个方法的区别,前者是绘制已定义的路径,后者是填充当前绘图(路径),如果这里我们用了 fill() ,那么我们将会得到一个黑色的圆形也不是一个只有弧线的圆。
canvas 中的角度其实都是以弧度为单位的数值,而不是直接写多少度,两者间的换算可以写个函数获得:
function degreesToRadian(degrees){ return degrees/ 180 * Math.PI; }
画完时钟的边框后我们紧接着调用 fillText(text, x, y, maxWidth) 方法画出时钟上12个小时数,我们在画之前先设置了文字的 textAlign 和 textBaseline 属性,如果我们没有设置这两个属性,那么我们会发现画出来的文字位置会发生一些偏差、不够整齐。然后我们再计算出各数字对应的坐标即可。
textAlign 属性规定文本内容的对齐方式:
![]()
textBaseline 属性规定在绘制文本的文本基线:
最后轮到画时钟上每一秒对应的点,通过计算获取各点的位置,通过改变其 fillStyle 属性来改变圆点的颜色,然后调用 fill() 方法画出小圆点点,在这里我们每隔4个灰色点就画一个黑色点。
画完背景之后就得到下面的效果图:
fillStyle 属性设置用于填充绘画的颜色、渐变或模式。
画时针、分针和秒针
//画时针
function drawHour(hour,minute){
ctx.save();
ctx.beginPath();
var rad = 2*Math.PI/12*hour;
var mrad = 2*Math.PI/12/60*minute;
ctx.rotate(rad + mrad);
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.moveTo(0, 10);
ctx.lineTo(0, -r/2);
ctx.stroke();
ctx.restore();
}
//画分针
function drawMinute(minute){
ctx.save();
ctx.beginPath();
var rad = 2*Math.PI/60*minute;
ctx.rotate(rad);
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.moveTo(0, 10);
ctx.lineTo(0, -r + 30);
ctx.stroke();
ctx.restore();
}
//画秒针
function drawSecond(second){
ctx.save();
ctx.beginPath();
ctx.fillStyle = 'mediumblue';
var rad = 2*Math.PI/60*second;
ctx.rotate(rad);
ctx.moveTo(-2, 20);
ctx.lineTo(2, 20);
ctx.lineTo(1, -r+18);
ctx.lineTo(-1, -r+18);
ctx.fill();
ctx.restore();
}
因为每次画指针都会用到旋转操作,所以在画之前我们需要先对画布状态进行保存,画完再还原。可以你会觉得有疑问,我们在画时钟之前不是保存了一次吗,都没恢复呢怎么又能再保存一次,事实上,Canvas 状态是以栈(stack)的方式保存的,每一次调用 save 方法,当前的状态就会入栈保存起来。这些状态包括:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX,shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 的值,我们可以调用任意多次 save() 方法。每一次调用 restore() 方法,上一个保存的状态就从栈中弹出,恢复到保存的状态。
接着我们根据传进来的参数计算出指针需要旋转的角度,注意时针的旋转角度要算上当前的分钟数产生的旋转角度,然后我们调用 rotate(angle) 方法旋转画布,然后我们再画出指针。
画指针时候,我们可以设置 lineCap 属性使时针和分针末端变成圆形线帽。这里需要注意 moveTo(x, y) 和 lineTo(x, y) 这两个方法的区别,前者相当于把笔移到(x, y)处,但并没有画线,后者是画一条线到(x, y)处。
lineCap 属性设置线条末端线帽的样式,默认为 butt(向线条的每个末端添加平直的边缘),还有两个值分别是 round(向线条的每个末端添加圆形线帽)和 square(向线条的每个末端添加正方形线帽)。
画原点
function drawDot(){
ctx.beginPath();
ctx.fillStyle = 'silver';
ctx.arc(0, 0, 3, 0, 2*Math.PI, false);
ctx.fill();
}
启动时钟
draw();
//每一秒画一次
setInterval(draw, 1000);
我们使用 setInterval 方法,每个一秒钟调用一次 draw() 函数画时钟,这样就实现了时钟动画:
美化
使用一张半透明的图片做时钟面的背景
//背景图
var img = new Image();
// 创建img元素
img.src = 'Itachi.jpg';
img.onload = function ()
{
ctx.save();
//设置透明度
ctx.globalAlpha=0.6;
ctx.drawImage(img,-75,-75,150,150);
ctx.globalAlpha=1;
ctx.restore();
}
设置 globalAlpha 属性设置画布的当前透明值,然后使用 drawImage(img,x,y,width,height) 方法画出图片。
globalAlpha 属性设置或返回绘图的当前透明值(alpha 或 transparency)。其属性值必须是介于 0.0(完全透明) 与 1.0(不透明) 之间的数字。
drawImage() 方法用于在画布上绘制图像、画布或视频。
关于 Image 对象的知识可以看看这里。
我们还可以将画布上的图片切成圆形的:
//背景图
var img = new Image();
// 创建img元素
img.src = 'Itachi.jpg';
img.onload = function ()
{
ctx.save();
//设置透明度
ctx.globalAlpha=0.6;
//画出圆形图片
circleImg(ctx, img, -180, -180, 180);
ctx.globalAlpha=1;
ctx.restore();
}
//画出圆形图片
function circleImg(ctx, img, x, y, r) {
ctx.save();
var d =2 * r;
var cx = x + r;
var cy = y + r;
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
ctx.clip();
ctx.drawImage(img, x, y, d, d);
ctx.restore();
}
这样就能得到一个比较美观的时钟了:
给指针添加发光效果
我们在画秒钟之前可以加上下面的代码:
//设置阴影
ctx.shadoeOffsetX=7;
ctx.shadoeOffsetY=7;
ctx.shadowBlur=5;
ctx.shadowColor="rgba(0,0,255,0.9)";
- shadowOffsetX:x轴方向阴影偏移量,默认0
- shadowOffsetY:y轴方向阴影偏移量,默认0
- shadowBlur:模糊像素
- shadowColor:阴影颜色,默认黑
看看效果吧:
优化
等比例缩放
现在写出来的时钟有一个问题就是,如果我们改变了画布的大小,那么时钟的内容会变大或变小,不能等比例缩放。所以我们需要写一个变量,这个变量获取 Canvas 和我们原始比例比较放大或缩小的倍数,然后我们在每次计算位置和大小的时候乘上这个变量就能实现等比例缩放了。
//比例
var rem = width/200;
这里除数为200的原因是因为我们一开始画布的大小就是200px * 200px 的正方形。
拿画时针为例来看看怎么乘这个比例:
function drawHour(hour,minute){
ctx.save();
ctx.beginPath();
var rad = 2*Math.PI/12*hour;
var mrad = 2*Math.PI/12/60*minute;
ctx.rotate(rad + mrad);
ctx.lineWidth = 6 * rem;
ctx.lineCap = 'round';
ctx.moveTo(0, 10 * rem);
ctx.lineTo(0, -r/2);
ctx.stroke();
ctx.restore();
}
随屏幕变化改变画布大小
var body = document.querySelector("body");
var height, width;
//重新调整画布大小
function resize(){
height = body.clientHeight;
width = body.clientWidth;
canvas.height = height * 0.5;
canvas.width = width * 0.5;
}
resize();
window.onresize = resize;
window 的 onresize 属性可以用来获取或设置当前窗口的resize事件的事件处理函数。在这里,在窗口大小改变之后,就会触发 resize 事件,我们可以在 resize() 中获取当前窗口的大小并对 Canvas 的大小进行修改。
关于 onresize 属性的知识可以看看这里。