实现效果
除了皮卡丘背景外的刻度线,时针分针秒针均是使用Canvas绘制出来的。
实现过程
设置基本大小及背景
1. 皮卡丘全脸素材
2. 基本结构
- 设置HTML结构如下:
<div class="canvas-box">
<canvas width="300" height="300"></canvas>
</div>
问题:这里为什么要外嵌一个div盒子呢?
因为后续我们通过给div设置背景图片background-image,使得时钟的背景是皮卡丘。而不直接给canvas设置背景图片的原因是背景图片会覆盖我们后面绘制的canvas时钟。
- 设置style样式如下:
.canvas-box {
width: 300px;
height: 300px;
background-image: url("img/pikachu.webp");
/* 拉伸图片,才看得到图片全景 */
background-size: contain;
/* 水平居中 + 竖直方向上移动一下*/
background-position: 50% 25%;
/* 如果图片够对称,可以如下设置 */
/* background-position: center; */
/* 变圆 */
border-radius: 50%;
}
基本API(按基本使用顺序)
- getContext:返回Canvas对象的上下文,可以理解为画笔。
- clearRect:把像素设置为透明以达到擦除一个矩形区域的目的,避免反复绘制。
- save:压栈保存canvas画笔状态。(对应restore)
- translate:对当前网格添加平移变换,以后的x,y都会改变平移的相对位置。
- rotate:变换矩阵中增加旋转,相当于之后的角度都会改变旋转的相对角度。
- beginPath:清空子路径列表开始一个新路径的方法,表示画笔开始绘制。
- arc:绘制圆弧路径
- moveTo:将一个新的子路径的起始点移动到(x,y)坐标
- lineTo:使用直线连接子路径的终点到x,y坐标
- stroke:使用非零环绕规则,根据当前的画线样式,绘制当前或已经存在的路径的方法。说白了就是描边。
- fillText:在 (x, y)位置填充文本。类似的有fillRect,fill。
- closePath:将笔点返回到当前子路径起始点的方法。它尝试从当前点到起始点绘制一条直线。 如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作。
- restore:弹栈恢复画笔状态。
当然,还有一些画笔的属性,比如lineWidth设置线宽,lineCap设置线端显示样式,fillStyle和strokeStyle分别设置填充的颜色以及线条颜色。
前置知识了解后,我们就开始正式绘制时钟啦~
绘制刻度及数字
大致内容:获取完时钟的基本信息后,依照保存-绘制-恢复的套路,先将画笔移至圆心,接着绘制一个圆弧作为边框。
如何设置数字和刻度的角度:在循环中均分一圈的角度(2*Math.PI),由于有12个数字,所以均分成12份,因为有60个刻度,所以刻度也需要均分成60份。
如何设置数字和刻度的坐标:通过单位圆中的正弦函数sin及余弦函数cos可以求到圆心相对的x,y坐标。当然,因为时钟不是单位圆,所以我们额外乘多一个(微调的)半径长度。
// 获取基本数据
let canvas = document.querySelector("canvas")
let ctx = canvas.getContext('2d')
let width = ctx.canvas.width
let height = ctx.canvas.height
let r = width / 2; //半径
// 绘制背景
function drawBackground() {
// 基本套路 压栈- 绘制 - 弹栈
ctx.save(); // 保存原有的上下文信息到栈中
ctx.translate(r, r); // 将画笔移到圆心
ctx.beginPath() // 开始绘制
ctx.lineWidth = 2 // 设置线宽
// 绘制时钟的边框
// 原型: void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
ctx.arc(0, 0, r - ctx.lineWidth / 2, 0, 2 * Math.PI) //相对0,0,半径为r-线宽一半绘制圆
ctx.closePath() // 结束绘制
ctx.strokeStyle = '#222' //设置填充颜色
ctx.stroke() //填充
// 刻度数字:由于圆的起始方向为x正方向,故起始为3
var hourNum = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2]
hourNum.map(function (num, i) {
var rad = 2 * Math.PI / 12 * i // 2 PI 是一圈 ,拆成12份
var x = Math.cos(rad) * (r * 10 / 12)
var y = Math.sin(rad) * (r * 10 / 12)
//设置接下来填充的文字样式
ctx.font = '13px Arial'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = '#000'
ctx.fillText(num, x, y)
})
// 绘制分钟刻度
for (var i = 0; i < 60; i++) {
var rad = 2 * Math.PI / 60 * i
var x = Math.cos(rad) * (r * 95 / 100)
var y = Math.sin(rad) * (r * 95 / 100)
ctx.beginPath()
if (i % 5 == 0) {
ctx.fillStyle = '#000'
ctx.arc(x, y, 1, 0, 2 * Math.PI, false)
} else {
ctx.fillStyle = '#808080'
ctx.arc(x, y, 1, 0, 2 * Math.PI, false)
}
ctx.fill()
}
ctx.restore(); // 还原原有的上下文信息
}
绘制时针,分针,秒针
大致内容:绘制时针其实等于从圆心处拉一条直线。但是我们需要根据一个hour来设置它指向的角度。不难理解,我们依旧拆分12个格子的角度(2 * Math.PI / 12),看hour是多少,那么就有多少份的角度。
小细节:如果依照hour,那么时针只会刚好指向整点,所以按照这个思路,我们需要再去切分更细的角度,然后根据minutes加上两个整点间的偏移。
// 绘制时针
function drawHour(hour, minute) {
ctx.save();
ctx.translate(r, r);
var rad = 2 * Math.PI / 12 * hour; //2Π 是一圈,再拆成12个格子
var mad = 2 * Math.PI / 12 / 60 * minute; // 对应分钟的比例
ctx.rotate(rad + mad);
ctx.beginPath();
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.moveTo(0, 0);
ctx.lineTo(0, -r / 3);
ctx.stroke();
ctx.restore();
}
分针和秒针就更简单了,只需要对应时针的套路改下线条的粗细,长度即可。
// 绘制分针
function drawMinute(minute) {
ctx.save();
ctx.translate(r, r);
let rad = 2 * Math.PI / 60 * minute; //2Π 是一圈,再拆成60个格子
ctx.rotate(rad);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.moveTo(0, 0);
ctx.lineTo(0, -r / 2);
ctx.stroke();
ctx.restore();
}
// 绘制秒针
function drawSecond(second) {
ctx.save()
ctx.translate(r, r);
var rad = 2 * Math.PI / 60 * second
ctx.rotate(rad)
ctx.beginPath()
ctx.lineWidth = 1
ctx.lineCap = 'round'
ctx.strokeStyle = 'red'
ctx.moveTo(0, 0)
ctx.lineTo(0, -r * 9 / 10);
ctx.stroke()
ctx.restore()
}
定时更新时间
最后我们只需要不断更新时间,即可实现动态时钟。
function drawMyClock() {
ctx.clearRect(0, 0, width, height) //清空,否则会重复绘制
let now = new Date();
let hour = now.getHours();
let minute = now.getMinutes();
let second = now.getSeconds();
drawBackground();
drawHour(hour, minute);
drawMinute(minute);
drawSecond(second);
}
drawMyClock(); //加载页面时直接调用一次
setInterval(function () { //设置定时器直接
drawMyClock();
}, 1000);
效果如下:
最后,我们尝试用class更加优雅地封装这个时钟,将相同的ctx状态,如translate(r,r)抽离到一个不断更新的函数refresh。即最终代码如下:
class MyClock {
constructor(canvas,isNeedBorder = true,isNeedNumberDot = true,isNeedMinuteDot = true) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.width = this.ctx.canvas.width
this.height = this.ctx.canvas.height
this.r = this.width / 2; //半径
this.isNeedBorder = isNeedBorder;
this.isNeedNumberDot = isNeedNumberDot;
this.isNeedMinuteDot = isNeedMinuteDot;
this.timer = null;
}
setTime(hour, minute, second) {//设置时钟时间
if (this.isValid(hour, minute, second)) {
this.hour = hour;
this.minute = minute;
this.second = second;
}
}
isValid(hour, minute, second) {//判断时间是否合法
if (typeof hour == 'number' && typeof minute == 'number' && typeof second == 'number' && hour >=
0 && hour < 24 && minute >= 0 && minute < 60 && second >= 0 && second < 60) {
return true;
}
return false;
}
start(hour, minute, second) {//开始运作时钟
let now = new Date();
this.setTime(hour || now.getHours(), minute || now.getMinutes(), second || now.getSeconds());
console.log(this.hour, this.minute, this.second);
this.timer = setInterval(() => {
this.refresh();
this.addTime();
}, 1000);
}
end(){//结束时钟
clearInterval(this.timer);
}
addTime() {//更新时间
this.second++;
if (this.second >= 60) {
this.second -= 60;
this.minute++;
if (this.minute >= 60) {
this.minute -= 60;
this.hour++;
if (this.hour >= 24) {
this.hour -= 24;
}
}
}
}
refresh() {//刷新时钟
this.ctx.save(); //此处调整公共的ctx上下文
this.ctx.clearRect(0, 0, this.width, this.height) //清空,否则会重复绘制
this.ctx.translate(this.r, this.r);
this.ctx.lineCap = 'round'
if (this.hour == undefined) return; //没有初始化时间
this.drawBackground(this.isNeedBorder,this.isNeedNumberDot,this.isNeedMinuteDot);
this.drawHour(this.hour, this.minute);
this.drawMinute(this.minute);
this.drawSecond(this.second);
this.ctx.restore();
}
drawBackground() {//绘制背景(边框+刻度)
let ctx = this.ctx;
if (this.isNeedBorder) {
this.drawBorder();
}
if (this.isNeedNumberDot) {
this.drawNumberDot();
}
if (this.isNeedMinuteDot) {
this.drawMinuteDot();
}
}
drawBorder() {//绘制边框
let ctx = this.ctx;
let r = this.r;
ctx.save(); // 保存原有的上下文信息到栈中
ctx.beginPath() // 开始绘制
ctx.lineWidth = 2 // 设置线宽
// 原型: void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
ctx.arc(0, 0, r - ctx.lineWidth / 2, 0, 2 * Math.PI) //相对0,0,半径为r-线宽一半绘制圆
ctx.closePath() // 结束绘制
ctx.strokeStyle = '#222' //设置填充颜色
ctx.stroke() //填充
ctx.restore(); // 还原原有的上下文信息
}
drawNumberDot() { //绘制数字刻度
let ctx = this.ctx;
let r = this.r;
ctx.save();
// 刻度数字:由于圆的起始方向为x正方向,故起始为3
var hourNum = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2]
hourNum.map(function (num, i) {
var rad = 2 * Math.PI / 12 * i // 2 PI 是一圈 ,拆成12份
var x = Math.cos(rad) * (r * 10 / 12)
var y = Math.sin(rad) * (r * 10 / 12)
//设置接下来填充的文字样式
ctx.font = '13px Arial'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = '#000'
ctx.fillText(num, x, y)
})
ctx.restore();
}
drawMinuteDot() { // 绘制分钟刻度
let ctx = this.ctx;
let r = this.r;
ctx.save();
for (var i = 0; i < 60; i++) {
var rad = 2 * Math.PI / 60 * i
var x = Math.cos(rad) * (r * 95 / 100)
var y = Math.sin(rad) * (r * 95 / 100)
ctx.beginPath()
if (i % 5 == 0) {
ctx.fillStyle = '#000'
ctx.arc(x, y, 1, 0, 2 * Math.PI, false)
} else {
ctx.fillStyle = '#808080'
ctx.arc(x, y, 1, 0, 2 * Math.PI, false)
}
ctx.fill()
ctx.closePath();
}
ctx.restore();
}
drawHour(hour, minute) {//绘制时针
let ctx = this.ctx;
let r = this.r;
ctx.save();
var rad = 2 * Math.PI / 12 * hour; //2Π 是一圈,再拆成12个格子
var mad = 2 * Math.PI / 12 / 60 * minute; // 对应分钟的比例
ctx.rotate(rad + mad);
ctx.beginPath();
ctx.lineWidth = 3;
ctx.moveTo(0, 0);
ctx.lineTo(0, -r / 3);
ctx.stroke();
ctx.restore();
}
drawMinute(minute) {//绘制分针
let r = this.r;
let ctx = this.ctx;
ctx.save();
let rad = 2 * Math.PI / 60 * minute; //2Π 是一圈,再拆成60个格子
ctx.rotate(rad);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(0, 0);
ctx.lineTo(0, -r / 2);
ctx.stroke();
ctx.restore();
}
drawSecond(second) {//绘制秒针
let ctx = this.ctx;
let r = this.r;
ctx.save()
var rad = 2 * Math.PI / 60 * second
ctx.rotate(rad)
ctx.beginPath()
ctx.lineWidth = 1
ctx.strokeStyle = 'red'
ctx.moveTo(0, 0)
ctx.lineTo(0, -r * 9 / 10);
ctx.stroke()
ctx.restore()
}
}
// 获取基本数据
let canvas = document.querySelector("canvas")
let yyClock = new MyClock(canvas);
yyClock.start();