这篇文章通过画一个时钟,让你了解 Canvas 中最常用的 API 的使用方法。就算你从来没有使用过它,也能大概了解 Canvas 能做什么事,以后工作中遇到了就能快速想起如何去使用它。
首先,我们先在 html 初始化一个 canvas 元素:
<canvas id="myCanvas" width="200" height="200" style="border: 1px solid black"></canvas>
画表盘
我们的表盘是由一大一小的两个圆型构成的,画表盘的核心 API 就是 Context.arc。
大致的使用如下:
ctx.arc(100, 100, 99, 0, 2 * Math.PI, true);
它表示着以(100,100)为圆心,以 99为半径画一个圆。坐标系的起点是我们页面中 canvas 的左上顶点。
第一二个参数分别是圆心的 x、y 轴的坐标,第三个参数是半径,第四个参数是画圆的起始弧度,第五个是画圆的终止弧度,最后一个参数是画圆是按照顺时针还是逆时针,true 的话是顺时针。
最终的效果如下:
为了更逼真一点,我们画两个圆来作为我们的表盘:
ctx.beginPath();
ctx.arc(100, 100, 99, 0, 2 * Math.PI, true);
ctx.moveTo(194, 100); // 为了防止画出多余的线段
ctx.arc(100, 100, 94, 0, 2 * Math.PI, true);
ctx.stroke();
你可能注意到了,我们多加了 ctx.moveTo(194, 100) 这一行代码,为什么呢?如果不加会如下图,画出多余的线段,
所以,我们要额外加那一行代码,先把画笔移动到内圆上。
画表心
有了上一步的基础,这一步就非常简单了,我们只是需要在圆心处画一个很小的实心圆。注意到没?上一步我们使用的是 ctx.stroke 来画圆,但是我们没有填充它的颜色。这一步我们就需要来填充颜色了,需要在最后一步使用 ctx.fill 来画。
ctx.beginPath(); // 这样也可以防止画出多余的线段
ctx.arc(100, 100, 4, 0, 2 * Math.PI, true);
ctx.fill();
效果如下图:
在不进行任何设置的时候,填充的默认颜色是黑色,你可以在调用 ctx.fill 之前使用 ctx.fillStyle 指定它的颜色:
ctx.fillStyle = "#ddd"
画时针分针
这一小节,我们学习如何画线段。使用到的 API 是 context.lineTo 和 context.moveTo。其实非常的简单粗暴。
刚开始,我们整个 canvas 的原点是在左上角,但是为了方便期间,现在把原点改为圆心处:
ctx.translate(100, 100);
接下来,我们就可以比较简单的画线了:
// 画分针
ctx.moveTo(0, 0);
ctx.lineTo(0, -80);
// 绘制时针
ctx.moveTo(0, 0);
ctx.lineTo(-45, 0);
ctx.stroke();
整体的效果如下:
有点模样了吧~
让表动起来
很多同学不知道,canvas 也是可以有动效的,这一小节我就为大家带来 canvas 最简单的动效如何实现。
为了讲述的简单,我们只让秒针动起来就可以了,因为只要了解了如何让一个针动起来,其他的也是易如反掌的事情。
我们把整个表表盘分成四个象限:
象限的划分和我们初中学的一样,只不过这里的 Y 轴朝下才是正向的。
首先我们需要两个工具函数:getTimeQuadrant 和 computePoint。
getTimeQuadrant 用来得到当前的时间在第几象限内:
function getTimeQuadrant(time) {
if (time >= 60) {
throw new Error('`time` is out of boundary');
}
return parseInt(time / 15) + 1;
}
我们可以使用 new Date().getSeconds() 来获取当前是第几秒,我们的 computePoint 函数,可以根据当前的秒数计算点的位置是什么:
function computePoint(r, time) {
let radian = time / 60 * 2 * Math.PI;
let computePointObj = {
1: () => {
return {
x: r * Math.sin(radian),
y: -r * Math.cos(radian)
}
},
2: () => {
return {
x: r * Math.sin(Math.PI - radian),
y: r * Math.cos(Math.PI - radian)
}
},
3: () => {
return {
x: -r * Math.cos(1.5 * Math.PI - radian),
y: r * Math.sin(1.5 * Math.PI - radian)
}
},
4: () => {
return {
x: -r * Math.sin(2 * Math.PI - radian),
y: -r * Math.cos(2 * Math.PI - radian)
}
}
}
return computePointObj[getTimeQuadrant(time)]()
}
接下来也是很粗暴的事情了,设置一个定时器,每隔一秒花一条线就行:
setInterval(() => {
ctx.beginPath()
ctx.moveTo(0, 0);
let { x, y } = computePoint(60, new Date().getSeconds());
ctx.lineTo(x, y)
ctx.closePath();
ctx.stroke();
}, 1000)
但是事实上这样是不行的,我们会产生如下的结果:
虽说还挺好看的,但这不是我们要的结果,具体产生这样的原因是什么呢?我们没有在下一秒划线之前,清除掉上一次的结果,我们加上,最后这段代码就是:
setInterval(() => {
// 清除操作,核心 API 是 clearRect
ctx.arc(0, 0, 4, 0, 2 * Math.PI, true);
ctx.clearRect(-60, -60, 120, 120);
ctx.arc(0, 0, 4, 0, 2 * Math.PI, true);
ctx.fill();
// 画秒针
ctx.beginPath()
ctx.moveTo(0, 0);
let { x, y } = computePoint(60, new Date().getSeconds());
ctx.lineTo(x, y)
ctx.closePath();
ctx.stroke();
}, 1000)
这样就能得到一个每秒都动一下的动效了,依照此原理,我们可以加上分针、秒针。
我们的例子很简单,但是基本上把 Canvas 常用的 API 都涉及到了,包括画圆、画线段、动效。剩下的就需要同学你自己去探索啦,希望你能有所收获,谢谢!
所有代码:
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(100, 100, 99, 0, 2 * Math.PI, true);
ctx.moveTo(194, 100);
ctx.arc(100, 100, 94, 0, 2 * Math.PI, true);
ctx.stroke();
ctx.beginPath(); // 这样也可以防止画出多余的线段
ctx.arc(100, 100, 4, 0, 2 * Math.PI, true);
ctx.fill();
ctx.translate(100, 100);
setInterval(() => {
ctx.arc(0, 0, 4, 0, 2 * Math.PI, true);
ctx.clearRect(-60, -60, 120, 120);
ctx.arc(0, 0, 4, 0, 2 * Math.PI, true);
ctx.fill();
// 画秒针
ctx.beginPath()
ctx.moveTo(0, 0);
let { x, y } = computePoint(60, new Date().getSeconds());
ctx.lineTo(x, y)
ctx.closePath();
ctx.stroke();
}, 1000)
function getTimeQuadrant(time) {
if (time >= 60) {
throw new Error('`time` is out of boundary');
}
return parseInt(time / 15) + 1;
}
function computePoint(r, time) {
let radian = time / 60 * 2 * Math.PI;
let computePointObj = {
1: () => {
return {
x: r * Math.sin(radian),
y: -r * Math.cos(radian)
}
},
2: () => {
return {
x: r * Math.sin(Math.PI - radian),
y: r * Math.cos(Math.PI - radian)
}
},
3: () => {
return {
x: -r * Math.cos(1.5 * Math.PI - radian),
y: r * Math.sin(1.5 * Math.PI - radian)
}
},
4: () => {
return {
x: -r * Math.sin(2 * Math.PI - radian),
y: -r * Math.cos(2 * Math.PI - radian)
}
}
}
return computePointObj[getTimeQuadrant(time)]()
}