Canvas实战:绘制一个时钟

1,525 阅读2分钟

最近学习了一些canvas基础语法,也复习了一些三角函数知识,于是决定基于现有学习的知识自己摸索来实现一个时钟功能。

基础工作

这里列举一些开发中要数学公式,这里要注意的是所有用到的API都为弧度制

  • 弧度 = 角度 * Math.PI / 180
  • 已知半径为r,圆心点为(0, 0)求圆上一点坐标:
    • x = Math.sin(弧度) * r
    • y = -Math.cos(弧度) * r

获取上下文

其次是一些获取canvas上下文等基本操作,这里我们使用到Class来实现,构造函数中配置一些基础信息,例如圆心、半径等。

class Clock {
    constructor(canvas, width, height, r) {
        this.canvas = canvas
        this.canvas.width = width
        this.canvas.height = height
        this.center = { x: width / 2, y: height / 2 } // 圆心
        this.r = r // 半径
        this.ctx = canvas.getContext('2d') // 获取渲染上下文
    }
}
const canvas = document.querySelector('#canvas')
const clock = new Clock(canvas, 500, 500, 200)

绘制表盘

首先绘制表盘,为Clock添加renderBorderrenderClockDial方法。

  • renderBorderrenderCenter主要是绘制时钟边框与圆心,用到的主要是arc方法,绘制一个以圆心为坐标,半径为r的圆,但这里需要偏移一些,不然会与数字重合,要注意的是这里startAngleendAngle都为弧度制,刚好2 * Math.PI为一个圆。

ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)

// 渲染表盘
renderBorder() {
    this.ctx.beginPath()
    const offset = 30 // 设置一些偏移量,不然会与后续数字重叠
    this.ctx.arc(this.center.x, this.center.y, this.r + offset, 0, 2 * Math.PI)
    this.ctx.closePath()
    this.ctx.stroke()
}
// 渲染圆心
renderCenter() {
    this.ctx.beginPath()
    const centerR = 5 // 设置一些偏移量,不然会与后续数字重叠
    this.ctx.arc(this.center.x, this.center.y, centerR, 0, 2 * Math.PI)
    this.ctx.closePath()
    this.ctx.fill()
}
  • renderClockDial方法为绘制圆盘1~12的数字,观察时钟,以12点为0度,1点为30度,以此类推刚好得出
    • 每个时间点的角度为360 / 12 * 小时数
    • 每个时间点的弧度为角度 * Math.PI / 180
    • 每个时间点的(x, y)坐标:x = Math.sin(弧度) * ry = -Math.cos(弧度)* r
// 渲染表盘数字
renderClockDial() {
    const ctx = this.ctx
    // 因为需要修改圆心点等状态,所以保存一下状态,后续恢复
    ctx.save()
    // 将原点移动到中心点,以便操作
    ctx.translate(this.center.x,  this.center.y)
    // 设置一些文字的基础样式
    ctx.textBaseline = 'middle'
    ctx.font = '30px san-self'
    ctx.textAlign = 'center'
    for(let hour = 1; hour <= 12; hour++) {
        const angle = (360 / 12) * hour
        const radian = angle * Math.PI / 180
        const x = Math.sin(radian) * this.r
        const y = -Math.cos(radian) * this.r
        const text = String(hour)
        ctx.fillText(text, x, y)
    }
    // 恢复状态
    ctx.restore()
}

image.png

绘制时针与分针

时针与分针其实类似,按照上面操作类似依葫芦画瓢,新建renderHoursrenderMinutes方法。

  • 通过Date对象获取当前小时分钟数。
  • 每个小时角度为360 / 12 * 小时数
  • 每个分钟角度为360 / 60 * 分钟数
  • 弧度为角度 * Math.PI / 180
  • 获取时间点(x, y)坐标,从中心点连线到(x, y)坐标
  • 时针短一些,分针长一些。
// 渲染小时
renderHours() {
    const ctx = this.ctx
    ctx.save()
    // 将原点移动到中心点,以便操作
    ctx.translate(this.center.x,  this.center.y)
    const hours = new Date().getHours()
    const width = 50
    ctx.beginPath()
    ctx.moveTo(0, 0)
    const angle = (360 / 12) * hours
    const radina = angle * Math.PI / 180
    const x = Math.sin(radina) * width
    const y = -Math.cos(radina) * width
    ctx.lineTo(x, y)
    ctx.closePath()
    ctx.stroke()
    ctx.restore()
}
// 渲染分钟
renderMinutes() {
    const ctx = this.ctx
    ctx.save()
    ctx.translate(this.center.x,  this.center.y)
    const minutes = new Date().getMinutes()
    const width = 100 // 长度
    ctx.beginPath()
    ctx.moveTo(0, 0)
    const angle = (360 / 60) * minutes
    const radina = angle * Math.PI / 180
    const x = Math.sin(radina) * width
    const y = -Math.cos(radina) * width
    ctx.lineTo(x, y)
    ctx.closePath()
    ctx.strokeWidth = width
    ctx.stroke()
    ctx.restore()
}

image.png

绘制秒针

这里我们做一个围绕着圆盘,变动的秒针,依葫芦画瓢,我们可以获取当前秒数的坐标。

  • 根据Date对象获取当前秒数。
  • 获取秒数对应的(x, y)坐标。
  • 画一个小圆。
// 渲染秒针
renderSeconds() {
    const ctx = this.ctx
    ctx.save()
    ctx.translate(this.center.x,  this.center.y)
    const seconds = new Date().getSeconds()
    const angle = (360 / 60) * seconds
    const radina = angle * Math.PI / 180
    const x = Math.sin(radina) * this.r
    const y = -Math.cos(radina) * this.r
    ctx.beginPath()
    ctx.arc(x, y, 3, 0, Math.PI * 2)
    ctx.closePath()
    ctx.fill()
    ctx.restore()
}

image.png

让时钟动起来

要想让canvas动起来,实际就是不断地清除canvas,再不断地绘制canvas

  • 新建一个clear方法,用以清除。
  • 新建一个render方法,用以绘制
  • 新建一个start方法,不断执行clearrender
// 启动
start() {
    setInterval(() => {
        this.clear()
        this.render()
    }, 1000)
}
// 渲染
render() {
    this.renderBorder()
    this.renderClockDial()
    this.renderMinutes()
    this.renderHours()
    this.renderSeconds()
}
// 清空
clear() {
    const { width, height } = this.canvas
    this.ctx.clearRect(0, 0, width, height)
}

2022-11-07 18-09-09.2022-11-07 18_09_38.gif

整体代码