Canvas实现自定义时钟

实现效果

除了皮卡丘背景外的刻度线,时针分针秒针均是使用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(按基本使用顺序)

  1. getContext:返回Canvas对象的上下文,可以理解为画笔。
  2. clearRect:把像素设置为透明以达到擦除一个矩形区域的目的,避免反复绘制。
  3. save:压栈保存canvas画笔状态。(对应restore)
  4. translate:对当前网格添加平移变换,以后的x,y都会改变平移的相对位置
  5. rotate:变换矩阵中增加旋转,相当于之后的角度都会改变旋转的相对角度
  6. beginPath:清空子路径列表开始一个新路径的方法,表示画笔开始绘制。
  7. arc:绘制圆弧路径
  8. moveTo:将一个新的子路径的起始点移动到(x,y)坐标
  9. lineTo:使用直线连接子路径的终点到x,y坐标
  10. stroke:使用非零环绕规则,根据当前的画线样式,绘制当前或已经存在的路径的方法。说白了就是描边。
  11. fillText:在 (x, y)位置填充文本。类似的有fillRect,fill。
  12. closePath:将笔点返回到当前子路径起始点的方法。它尝试从当前点到起始点绘制一条直线。 如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作。
  13. 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();

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如果皮卡会coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值