最近看了一篇10分钟入门canvas,我熬夜写了3个小项目的文章,跟着做了一遍感觉受益匪浅,结合了这篇文章的精要讲讲如何使用canvas绘制时钟
先上最终效果图
整个实现的过程分为4步
- 通过translate将canvas变换的坐标点位移到canvas中心
- 画出刻度线
- 画文字
- 画时分秒针
理解第一步,何为变换中心
在canvas的变换里(旋转,缩放,移动),所有方法的基点坐标都是在canvas的左上角(0,0)的位置
灵魂画师:
不难看出一个时钟的刻度和时针都可以通过围绕中心点旋转得到,所以绘制canvas时钟的第一步就是将变换中心点移动到整个画布的中心.
translate坐标系位移
这个方法可以对canvas坐标系进行整体位移,详见MDN
示意图:
黑色为原始坐标系,红色为移动后的坐标系。
理解了以上就可以开始画图啦,先从一个圆开始
<canvas id="canvas" width="600" height="600"></canvas>
// 先移动坐标系
ctx.translate(300, 300)
// 画圆
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
由于移动了坐标系此时300,300变成了坐标的0,0
得到了一个在画布中心的圆
画出刻度线
ctx.save()
// 画刻度线
ctx.lineWidth = 1;
for (let i = 0; i < 60; i++) {
// 6 = 360/60
ctx.rotate(6 * Math.PI / 180)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
角度转弧度计算公式是:radian = degree * Math.PI / 180。例如,旋转45°,旋转弧度就是45 * Math.PI / 180。
这里的刻度有60个,所以每一个刻度就是旋转6 * Math.PI / 180
得到如下:
save()与restore()
save与restore是canvas的转态管理方法,分别是存储当前canvas的状态以及恢复canvas到前一次的状态
我们用于旋转的rotate方法是对整个canvas的坐标系进行旋转,所以在把刻度画好后调用restore()恢复坐标系,不然后续的制图都是画歪来的.
画时间文字
ctx.font = '16px STheiti,SimHei';
for (let i = 0; i < 12; i++) {
// 30 = 360/12
let numbers = ['Ⅰ','Ⅱ','Ⅲ','Ⅳ','Ⅴ','Ⅵ','Ⅶ','Ⅷ','Ⅸ','Ⅹ','Ⅺ','Ⅻ'];
ctx.rotate(30 * Math.PI / 180)
ctx.fillText(numbers[i], -8, -60)
}
画时针
// 获取当前时间
let time = new Date();
let hour = time.getHours() % 12;
let min = time.getMinutes();
let sec = time.getSeconds();
// 画时针
ctx.rotate((360 / 12 * hour - 90) * Math.PI / 180)
ctx.beginPath()
ctx.lineWidth = 10;
ctx.moveTo(-10, 0)
ctx.lineTo(40, 0)
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()
画时针这里解释一下为什么(360 / 12 * hour - 90)要减去90
灵魂画师又来啦:
默认情况下一条线指向的是三点钟方向,减去90°指针指向12点钟方向,然后才计算实际时间所在方位,或者你的一天是从3点钟开始计算的话,那么这步可以省略
完整代码如下:
class Clock {
constructor(canvas) {
this.ctx = canvas.getContext('2d');
}
drawClock() {
const ctx = this.ctx;
// 清除画布
ctx.save()
ctx.clearRect(0, 0, 600, 600)
this.ctx.translate(300, 300)
// 画圆线使用arc(中心点x,中心点y,半径,起始角度,结束角度)
// 画大圆
ctx.beginPath()
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
// 划线
ctx.stroke()
ctx.closePath()
// 划小圆
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
ctx.save()
// 画刻度线
ctx.lineWidth = 1;
for (let i = 0; i < 60; i++) {
// 6 = 360/60
ctx.rotate(6 * Math.PI / 180)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
// 画时针
ctx.lineWidth = 5;
for (let i = 0; i < 12; i++) {
// 30 = 360/12
ctx.rotate(30 * Math.PI / 180)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
// 画数字
ctx.font = '16px STheiti,SimHei';
for (let i = 0; i < 12; i++) {
// 30 = 360/12
let numbers = ['Ⅰ','Ⅱ','Ⅲ','Ⅳ','Ⅴ','Ⅵ','Ⅶ','Ⅷ','Ⅸ','Ⅹ','Ⅺ','Ⅻ'];
ctx.rotate(30 * Math.PI / 180)
ctx.fillText(numbers[i], -8, -60)
}
// 画时分秒
// 获取当前时间
let time = new Date();
let hour = time.getHours() % 12;
let min = time.getMinutes();
let sec = time.getSeconds();
// 画时针
ctx.rotate((360 / 12 * hour - 90) * Math.PI / 180)
ctx.beginPath()
ctx.lineWidth = 10;
ctx.moveTo(-10, 0)
ctx.lineTo(40, 0)
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()
// 画分针
ctx.rotate((360 / 60 * min - 90) * Math.PI / 180)
ctx.beginPath()
ctx.lineWidth = 5;
ctx.strokeStyle = 'blue';
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()
// 画秒针
ctx.rotate((360 / 60 * sec - 90) * Math.PI / 180)
ctx.beginPath()
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.restore()
}
run() {
this.timer = setInterval(() => {
this.drawClock()
}, 1000)
}
stop() {
clearInterval(this.timer)
}
}
const clock = new Clock(document.querySelector('canvas'));
clock.run()