用 js canvas 做一个优雅的模拟时钟, canvas 教程实例

有很多次,我都想找到一个比较不错的,可以查看模拟时钟的网页。
有时候是想看下距离某个时间点还有多长时间,有时候是想看一下,两个时间点之间的间隔是多少。因为模拟时钟的排布比数字时钟要更直观。
但一直没有找到。

这些天闲的时候就想做个 canvas 模拟时钟玩,慢慢就做出效果来了。
目前的大致效果如下:

可访问的地址:

线上地址(白): http://kylebing.cn/tools/clock-a/ 线上地址(黑): http://kylebing.cn/tools/clock-a?theme=black github: https://github.com/KyleBing/animate-clock-canvas

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_Math

一、需求

我对模拟时钟的需求有几个:

  • 一定要优雅,不花哨
  • 秒针一定要动的平滑,不是一秒一秒的动,而是要像 iOS 中的时钟那样
  • 优雅,还是优雅,简洁而不简单

二、实现原理:表盘

你需要具备使用 canvas 绘制简单图形的能力,不需要复杂,会画个方块、圆、文字,就可以了。
很简单,直接看 MDN 官方文档上的例子就能看明白。

CanvasRenderingContext2D: canvas property

会了基础的操作之后,就说一下粗略的实现过程:

<!DOCTYPE html>
<html lang="zh-CN">
<body>

<canvas id='canvas' width="600" height="600" style="width: 600px; height: 600px; border: 1px solid black;"></canvas>


</body>

<script>
    let canvasItem = document.querySelector('#canvas')
    let ctx = canvasItem.getContext("2d")

    ctx.fillStyle = 'magenta'
    ctx.fillRect(0,0, 5, 20)
</script>
</html>

此时画面是这样的

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_i++_02

1. 画12个小时的刻度

这里需要你会对 ctx 进行旋转,Math.PI 是半个圆的旋转角度,那么整个圆12等分后,每个刻度的旋转角度就是

let rotationAngle = Math.PI * (1/6)

这里需要注意的是,当你用 ctx.rotate() 方法旋转的时候,你旋转的是整个画布,并且它不会自动恢复到原来的样子。
也就是说,如果你 rotate() 了30度,那么画布就会在后面的绘画过程中都保持在 旋转了30度的这个状态。后面画的所有内容都是旋转 30 度的。

而这不是我们想要的,我们只需要在画这个表盘的时候旋转,在绘制完成之后恢复到原来的样子。

此时就需要 ctx 的两个方法了: save() restore() 这两个方法的作用就是 save() 保存画布的一个状态,在后面再 restore() 恢复到保存状态时的状态。

比如我们在这个画布上旋转30度再写个文字试试:

<!DOCTYPE html>
<html lang="zh-CN">
<body>

<canvas id='canvas' width="600" height="600" style="width: 600px; height: 600px; border: 1px solid black;"></canvas>


</body>

<script>
    let canvasItem = document.querySelector('#canvas')
    let ctx = canvasItem.getContext("2d")

    ctx.rotate(Math.PI / 6)  // 旋转 30 度
    ctx.fillStyle = 'magenta'
    ctx.fillRect(0,0, 5, 20)

    ctx.font = '50px Impact'  // 字体设置
    ctx.fillText('Rotate 30', 100,0)  // 绘制字体
</script>
</html>

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_javascript_03


但是如果我们使用 save() 和 restore() 就能保证文字的角度正常了。

<!DOCTYPE html>
<html lang="zh-CN">
<body>

<canvas id='canvas' width="600" height="600" style="width: 600px; height: 600px; border: 1px solid black;"></canvas>


</body>

<script>
    let canvasItem = document.querySelector('#canvas')
    let ctx = canvasItem.getContext("2d")

    ctx.save()  // 保存旋转之前的画布状态
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'magenta'
    ctx.fillRect(0,0, 5, 20)

    ctx.restore()
    ctx.font = '50px Impact'
    ctx.fillText('Rotate 30', 100,50)
</script>
</html>

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_ecmascript_04

好,知道这些了就开始绘制表盘的 12 个刻度了。

let canvasItem = document.querySelector('#canvas')
    let ctx = canvasItem.getContext("2d")

    ctx.save()  // 保存旋转之前的画布状态

    for (let i=0;i<12;i++){
        ctx.rotate(Math.PI / 6)
        ctx.fillStyle = 'magenta'
        ctx.fillRect(0,0, 5, 20)
    }

    ctx.restore()
    ctx.font = '50px Impact'
    ctx.fillText('Rotate 30', 100,50)

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_ecmascript_05

能看到上图中,这几个旋转是旋转了,但中心点不对,这里需要另外一个方法,就是 translate() ,用它将左上角的 0,0 点移动到画布的中间位置,再绘制。

let canvasItem = document.querySelector('#canvas')
let ctx = canvasItem.getContext("2d")

ctx.save()  // 保存旋转之前的画布状态

ctx.translate(300, 300) // 将 0,0 移动到画布中间

for (let i=0;i<12;i++){
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'magenta'
    ctx.fillRect(0,0, 5, 20)
}

ctx.restore()
ctx.font = '50px Impact'
ctx.fillText('Rotate 30', 100,50)

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_javascript_06

加个辅助线,再看,你应该参看到所有刻度线都没有位于中间。

let canvasItem = document.querySelector('#canvas')
let ctx = canvasItem.getContext("2d")

ctx.save()  // 保存旋转之前的画布状态

ctx.translate(300, 300) // 将 0,0 移动到画布中间

for (let i=0;i<12;i++){
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'magenta'
    ctx.fillRect(0,0, 10, 30)
}

ctx.restore()

drawRefLines(ctx, {x: 300, y:300})  // 画参考线

ctx.font = '50px Impact'
ctx.fillText('Rotate 30', 100,50)


function drawRefLines(ctx, center){
    const lineLength = 600
    ctx.save()
    ctx.beginPath()
    ctx.moveTo(center.x - lineLength/2, center.y)
    ctx.lineTo(center.x + lineLength/2, center.y)
    ctx.moveTo(center.x, center.y - lineLength/2)
    ctx.lineTo(center.x, center.y + lineLength/2)
    ctx.strokeStyle = 'magenta'
    ctx.strokeWidth = 1
    ctx.closePath()
    ctx.stroke()
    ctx.restore()
}

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_ecmascript_07


所以需要处理一下偏移量

for (let i = 0; i < 12; i++) {
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'magenta'
    ctx.fillRect(-5, 0, 10, 30)
}

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_开发语言_08


这样就能位于中间了,接下来就是需要将刻度偏移到外围,而不是聚在中间。这个只需要在绘制它的时候在 y 轴上添加一个距离中心点的偏移量即可。比如偏移 200

for (let i = 0; i < 12; i++) {
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'magenta'
    ctx.fillRect(-5, 200, 10, 30)
}

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_Math_09

2. 画60个的分钟刻度

跟上面的小时一样,只不过这个分钟的可以调小,它的角度是 Math.PI * (1/30) ,就是半个圆的 30 等分角度,也就是:

let canvasItem = document.querySelector('#canvas')
let ctx = canvasItem.getContext("2d")

// 小时刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 12; i++) {
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'magenta'
    ctx.fillRect(-5, 200, 10, 30)
}
ctx.restore()

// 画分钟刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 60; i++) {
    ctx.rotate(Math.PI / 30)
    ctx.fillStyle = 'gray'
    ctx.fillRect(-2, 200, 4, 20)
}
ctx.restore()

// 画参考线
drawRefLines(ctx, {x: 300, y:300})

ctx.font = '50px Impact'
ctx.fillText('Rotate 30', 100,50)


function drawRefLines(ctx, center){
    const lineLength = 600
    ctx.save()
    ctx.beginPath()
    ctx.moveTo(center.x - lineLength/2, center.y)
    ctx.lineTo(center.x + lineLength/2, center.y)
    ctx.moveTo(center.x, center.y - lineLength/2)
    ctx.lineTo(center.x, center.y + lineLength/2)
    ctx.strokeStyle = 'magenta'
    ctx.strokeWidth = 1
    ctx.closePath()
    ctx.stroke()
    ctx.restore()
}

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_javascript_10


能看到分钟的刻度在小时的刻度之上,因为分钟是后绘制的,为了避免这种情况,就需要将分钟先绘制,这两个调换一下顺序就好。

同时把小时的刻度改成黑色 black

// 画分钟刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 60; i++) {
    ctx.rotate(Math.PI / 30)
    ctx.fillStyle = 'gray'
    ctx.fillRect(-2, 200, 4, 20)
}
ctx.restore()

// 小时刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 12; i++) {
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'black'
    ctx.fillRect(-5, 200, 10, 30)
}
ctx.restore()

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_i++_11


为了外圈统一高度,需要平衡一下这种刻度的偏移量,以小时的刻度为准,分钟的刻度在绘制的时候就需要调整下:

ctx.fillRect(-2, 200 + 10, 4, 20)

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_开发语言_12

3. 画60 * 3 个的分钟刻度

咱这个例子的画布精度没有设置那么高,所以就不在每秒中间设置5个间隔了,改成3个。

那么每个间隔的旋转角度就是 Math.PI * (1/30) * (1/3),一圈就是 60 * 3 = 180 个

let canvasItem = document.querySelector('#canvas')
let ctx = canvasItem.getContext("2d")


// 画秒刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 180; i++) {
    ctx.rotate(Math.PI / 30 * (1/3))
    ctx.fillStyle = 'gray'
    ctx.fillRect(-1, 200 + 20, 2, 10)
}
ctx.restore()

// 画分钟刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 60; i++) {
    ctx.rotate(Math.PI / 30)
    ctx.fillStyle = 'gray'
    ctx.fillRect(-2, 200 + 10, 4, 20)
}
ctx.restore()

// 小时刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 12; i++) {
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'black'
    ctx.fillRect(-5, 200, 10, 30)
}
ctx.restore()



// 画参考线
drawRefLines(ctx, {x: 300, y:300})

ctx.font = '50px Impact'
ctx.fillText('Rotate 30', 100,50)


function drawRefLines(ctx, center){
    const lineLength = 600
    ctx.save()
    ctx.beginPath()
    ctx.moveTo(center.x - lineLength/2, center.y)
    ctx.lineTo(center.x + lineLength/2, center.y)
    ctx.moveTo(center.x, center.y - lineLength/2)
    ctx.lineTo(center.x, center.y + lineLength/2)
    ctx.strokeStyle = 'magenta'
    ctx.strokeWidth = 1
    ctx.closePath()
    ctx.stroke()
    ctx.restore()
}

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_i++_13


结果就是上面这样,能看到每个分钟间隔内等分了3份。

好像看起来不是太好看,只需要调整下刻度的大小就好了。这些刻度的大小最后也可以摘取成配置项,方便修改,我 github 已写好的程序上就都改成了配置项。

这里我把分钟和秒的刻度都改成同样宽度的线条,只是高度不一样,结果就是下面这样。

// 画秒刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 180; i++) {
    ctx.rotate(Math.PI / 30 * (1/3))
    ctx.fillStyle = 'gray'
    ctx.fillRect(-1, 200 + 20, 2, 10)
}
ctx.restore()

// 画分钟刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 60; i++) {
    ctx.rotate(Math.PI / 30)
    ctx.fillStyle = 'gray'
    ctx.fillRect(-1, 200 + 10, 2, 20)
}
ctx.restore()

// 小时刻度
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
for (let i = 0; i < 12; i++) {
    ctx.rotate(Math.PI / 6)
    ctx.fillStyle = 'black'
    ctx.fillRect(-2, 200, 4, 30)
}
ctx.restore()

质感一下子就来了。

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_Math_14

好了,到此,表盘已经画好了。

三、实现原理:时针、分针、秒针

1. 刷新画面内容

上面这些步骤的内容都是死的,就是独帧的内容,如果想让它动起来,就需要让它一次次的刷新里面的内容。
这里就需要补一下关于 canvas 的动画知识。

canvas 的内容刷新的驱动并不是靠 setInterval 这种定时执行的,而是靠浏览器自带的一个方法:

window.requestAnimationFrame()

这个方法内部执行的就是 canvas 下次刷新需要执行的方法。一般它的执行频率是你所用设备的屏幕刷新率,比如你的电脑屏幕刷新率是 30hz,那它就是每秒执行30次,手机一般高刷的就是 120hz,也就是每秒执行 120 次。

它还有一个优点就是,在界面没有被展示的时候,它是不会被执行的。也就是说,当画面切于后台时,不执行,只有显示在前台的时候才会被执行。而 setInterval 方法会在界面从后台切到前台显示时成堆的压到一直执行,所以它不适合作动画。

这个方法的意思是就是 “ 当前画面已经绘完了,我要绘制下一副内容了,我应该怎么绘制?”

我们这个例子里,其实下一帧需要绘制的内容还是我们之前绘制表盘的整个方法,这里我们就需要做一下整理了,把它整理成一个方法。
一般我们把这个方法名命名为了 draw()

将我们之前代码整理之后就是这样:
我加了一个名为 timeLine 的变量,用于记录画面刷新的总次数,每次绘制的时候都 + 1

<!DOCTYPE html>
<html lang="zh-CN">
<body>

<canvas id='canvas' width="600" height="600" style="width: 600px; height: 600px; border: 1px solid black;"></canvas>


</body>

<script>

    let timeLine = 0

    window.addEventListener('load', () => {
        draw()
    })

    function draw() {
        timeLine = timeLine + 1


        let canvasItem = document.querySelector('#canvas')
        let ctx = canvasItem.getContext("2d")
        // 画秒刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 180; i++) {
            ctx.rotate(Math.PI / 30 * (1 / 3))
            ctx.fillStyle = 'gray'
            ctx.fillRect(-1, 200 + 20, 2, 10)
        }
        ctx.restore()

        // 画分钟刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 60; i++) {
            ctx.rotate(Math.PI / 30)
            ctx.fillStyle = 'gray'
            ctx.fillRect(-1, 200 + 10, 2, 20)
        }
        ctx.restore()

        // 小时刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 12; i++) {
            ctx.rotate(Math.PI / 6)
            ctx.fillStyle = 'black'
            ctx.fillRect(-2, 200, 4, 30)
        }
        ctx.restore()

        // 画参考线
        drawRefLines(ctx, {x: 300, y: 300})

        ctx.font = '50px Impact'
        ctx.fillText(timeLine, 100, 50)
    }


    function drawRefLines(ctx, center) {
        const lineLength = 600
        ctx.save()
        ctx.beginPath()
        ctx.moveTo(center.x - lineLength / 2, center.y)
        ctx.lineTo(center.x + lineLength / 2, center.y)
        ctx.moveTo(center.x, center.y - lineLength / 2)
        ctx.lineTo(center.x, center.y + lineLength / 2)
        ctx.strokeStyle = 'magenta'
        ctx.strokeWidth = 1
        ctx.closePath()
        ctx.stroke()
        ctx.restore()
    }

</script>
</html>

上面这个内容还没有添加让它动起来的方法,所以它的显示是这样:

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_i++_15

我们把上面提到的方法添加上,它应该添加到 draw() 方法的末端。

function draw(){
        // 刷新画面内容
        window.requestAnimationFrame(draw)
}

这样再刷新页面的时候能看到这样的结果:

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_ecmascript_16


这是为什么呢? 这是因为在这一次次的 draw() 过程中,我们并没有在每次刷新之前清空画布,所以它就会将所有的内容都重叠在了一起。

我们需要做的就是在每次绘制内容之前执行一下 ctx.clearRect(0,0, 600,600) 清空画布。

<!DOCTYPE html>
<html lang="zh-CN">
<body>

<canvas id='canvas' width="600" height="600" style="width: 600px; height: 600px; border: 1px solid black;"></canvas>


</body>

<script>

    let timeLine = 0

    window.addEventListener('load', () => {
        draw()
    })

    function draw() {

        timeLine = timeLine + 1


        let canvasItem = document.querySelector('#canvas')
        let ctx = canvasItem.getContext("2d")

        // 清空画布
        ctx.clearRect(0,0, 600,600)

        // 画秒刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 180; i++) {
            ctx.rotate(Math.PI / 30 * (1 / 3))
            ctx.fillStyle = 'gray'
            ctx.fillRect(-1, 200 + 20, 2, 10)
        }
        ctx.restore()

        // 画分钟刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 60; i++) {
            ctx.rotate(Math.PI / 30)
            ctx.fillStyle = 'gray'
            ctx.fillRect(-1, 200 + 10, 2, 20)
        }
        ctx.restore()

        // 小时刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 12; i++) {
            ctx.rotate(Math.PI / 6)
            ctx.fillStyle = 'black'
            ctx.fillRect(-2, 200, 4, 30)
        }
        ctx.restore()

        // 画参考线
        drawRefLines(ctx, {x: 300, y: 300})

        ctx.font = '50px Impact'
        ctx.fillText(timeLine, 100, 50)

        // 刷新画面内容
        window.requestAnimationFrame(draw)
    }


    function drawRefLines(ctx, center) {
        const lineLength = 600
        ctx.save()
        ctx.beginPath()
        ctx.moveTo(center.x - lineLength / 2, center.y)
        ctx.lineTo(center.x + lineLength / 2, center.y)
        ctx.moveTo(center.x, center.y - lineLength / 2)
        ctx.lineTo(center.x, center.y + lineLength / 2)
        ctx.strokeStyle = 'magenta'
        ctx.strokeWidth = 1
        ctx.closePath()
        ctx.stroke()
        ctx.restore()
    }

</script>
</html>

结果就是这样

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_ecmascript_17

2. 绘制秒针

我们每次刷新都执行 draw() 这个方法,在每次刷新之后我们都需要重新获取一下当前时间,再根据时间去绘制每个指针。

比如我们通过当前时间可以获取到

  • new Date().getSeconds()
  • new Date().getMinutes()
  • new Date().getHours()

上面过程中我们已经学会绘制一个随角度变化再变化的指针了。
拿秒针为例,我们只需要计算出此时此刻的秒针应该旋转的角度就可以了。

我们先绘制一个不会动的秒针,在 draw() 里添加如下内容

// 绘制秒针
   ctx.save()
   ctx.translate(300, 300) // 将 0,0 移动到画布中间
   ctx.fillStyle = 'green'
   ctx.fillRect(-10, 0, 20, 200)
   ctx.restore()

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_ecmascript_18


能看到,指针的初始方向并不是我们想要的角度,我们需要的初始位置在上面,也就是相差一个 Math.PI 的角度,这里我们选择 + Math.PI

// 绘制秒针
  ctx.save()
  ctx.translate(300, 300) // 将 0,0 移动到画布中间
  ctx.rotate(Math.PI)
  ctx.fillStyle = 'green'
  ctx.fillRect(-10, 0, 20, 200)
  ctx.restore()

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_ecmascript_19


好了,现在我们就要让它动起来了。

上面我们已经可以获取到当前的秒数 new Date().getSeconds()
对应的旋转角度是,半圆的 30 等分。

let seconds = new Date().getSeconds()
let rotateAngle = Math.PI * (1/30) * seconds
// 绘制秒针
ctx.save()
ctx.translate(300, 300) // 将 0,0 移动到画布中间
let seconds = new Date().getSeconds()
let rotateAngle = Math.PI * (1/30) * seconds
ctx.rotate(Math.PI + rotateAngle)
ctx.fillStyle = 'green'
ctx.fillRect(-10, 0, 20, 200)
ctx.restore()

效果就是:

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_javascript_20


它现在是按秒动的,我们需要它动的更平滑。

同时我们把秒针改成细长的红色。

这里就需要引入时间对象中一个不太常用的 毫秒 ms 值了,1 秒 = 1000 毫秒

const seconds = new Date().getSeconds()
const ms = new Date().getMilliseconds()

那么秒针的准确值就应该是 秒 + 毫秒,每秒是 1000 毫秒,那它的实时角度就应该是

const rotateAngle = Math.PI * (1/30) * seconds + ms / 1000 * Math.PI / 30

放进原程序后的完成代码就是

<!DOCTYPE html>
<html lang="zh-CN">
<body>

<canvas id='canvas' width="600" height="600" style="width: 600px; height: 600px; border: 1px solid black;"></canvas>


</body>

<script>

    let timeLine = 0

    window.addEventListener('load', () => {
        draw()
    })

    function draw() {

        timeLine = timeLine + 1


        let canvasItem = document.querySelector('#canvas')
        let ctx = canvasItem.getContext("2d")

        // 清空画布
        ctx.clearRect(0,0, 600,600)

        // 画参考线
        drawRefLines(ctx, {x: 300, y: 300})

        // 画秒刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 180; i++) {
            ctx.rotate(Math.PI / 30 * (1 / 3))
            ctx.fillStyle = 'gray'
            ctx.fillRect(-1, 200 + 20, 2, 10)
        }
        ctx.restore()

        // 画分钟刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 60; i++) {
            ctx.rotate(Math.PI / 30)
            ctx.fillStyle = 'gray'
            ctx.fillRect(-1, 200 + 10, 2, 20)
        }
        ctx.restore()

        // 小时刻度
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        for (let i = 0; i < 12; i++) {
            ctx.rotate(Math.PI / 6)
            ctx.fillStyle = 'black'
            ctx.fillRect(-2, 200, 4, 30)
        }
        ctx.restore()


        // 绘制秒针
        ctx.save()
        ctx.translate(300, 300) // 将 0,0 移动到画布中间
        const seconds = new Date().getSeconds()
        const ms = new Date().getMilliseconds()

        const rotateAngle = Math.PI * (1/30) * seconds + ms / 1000 * Math.PI / 30
        ctx.rotate(Math.PI + rotateAngle)
        ctx.fillStyle = 'red'
        ctx.fillRect(-2, 0, 4, 220)
        ctx.restore()


        ctx.font = '50px Impact'
        ctx.fillText(timeLine, 100, 50)

        // 刷新画面内容
        window.requestAnimationFrame(draw)
    }


    function drawRefLines(ctx, center) {
        const lineLength = 600
        ctx.save()
        ctx.beginPath()
        ctx.moveTo(center.x - lineLength / 2, center.y)
        ctx.lineTo(center.x + lineLength / 2, center.y)
        ctx.moveTo(center.x, center.y - lineLength / 2)
        ctx.lineTo(center.x, center.y + lineLength / 2)
        ctx.strokeStyle = 'magenta'
        ctx.strokeWidth = 1
        ctx.closePath()
        ctx.stroke()
        ctx.restore()
    }

</script>
</html>

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_Math_21

怎么样,这样就变得很丝滑了。

接下来就是你自己需要去添加分针和时针了。上面就是完整的实例代码,直接保存为 html,在浏览器中打开就能运行。

给个提示:

  • 分针也是需要计算 ms s 来计算角度的,不然它只会在分钟变化时变化。
  • 时针需要考虑 s min, ms 可以不用考虑。
  • 你可能需要让它自动计算画面的最中心点在哪。
  • 慢慢去一点点的优化它,直到你理想的状态。

四、最终效果

线上地址: http://kylebing.cn/tools/clock-a/ github: https://github.com/KyleBing/animate-clock-canvas

结语:canvas 是个非常好玩的玩意,可以做你想做的任意效果,发挥你的想象力去创造有趣的东西吧。

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_javascript_22


完善,添加指针阴影、中心点连接块。

用 js canvas 做一个优雅的模拟时钟, canvas 教程实例_Math