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

news2024/11/27 0:48:36

用 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

在这里插入图片描述

一、需求

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

  • 一定要优雅,不花哨
  • 秒针一定要动的平滑,不是一秒一秒的动,而是要像 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>

此时画面是这样的
在这里插入图片描述

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>

在这里插入图片描述
但是如果我们使用 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>

在这里插入图片描述

好,知道这些了就开始绘制表盘的 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)

在这里插入图片描述

能看到上图中,这几个旋转是旋转了,但中心点不对,这里需要另外一个方法,就是 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)

在这里插入图片描述

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

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()
}

在这里插入图片描述
所以需要处理一下偏移量


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

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

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

在这里插入图片描述

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()
}

在这里插入图片描述
能看到分钟的刻度在小时的刻度之上,因为分钟是后绘制的,为了避免这种情况,就需要将分钟先绘制,这两个调换一下顺序就好。
同时把小时的刻度改成黑色 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()

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

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

在这里插入图片描述

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()
}

在这里插入图片描述
结果就是上面这样,能看到每个分钟间隔内等分了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()

质感一下子就来了。
在这里插入图片描述

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

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

1. 刷新画面内容

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

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

window.requestAnimationFrame()

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

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

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

我们这个例子里,其实下一帧需要绘制的内容还是我们之前绘制表盘的整个方法,这里我们就需要做一下整理了,把它整理成一个方法。
一般我们把这个方法名命名为了 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>

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

在这里插入图片描述

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

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

这样再刷新页面的时候能看到这样的结果:
在这里插入图片描述
这是为什么呢? 这是因为在这一次次的 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>

结果就是这样

在这里插入图片描述

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()

在这里插入图片描述
能看到,指针的初始方向并不是我们想要的角度,我们需要的初始位置在上面,也就是相差一个 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()

在这里插入图片描述
好了,现在我们就要让它动起来了。

上面我们已经可以获取到当前的秒数 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()

效果就是:

在这里插入图片描述
它现在是按秒动的,我们需要它动的更平滑。
同时我们把秒针改成细长的红色。

这里就需要引入时间对象中一个不太常用的 毫秒 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>

在这里插入图片描述

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

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

给个提示:

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

四、最终效果

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

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2045084.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

迅为i.MX8MM开发板控制GPIO高低电平-使用命令控制GPIO

在文件系统的/sys 目录下给用户提供了许多接口&#xff0c;比如在/sys 下面的 bus 目录&#xff0c;这个目录下放置的都是 和总线相关的&#xff0c;比如 I2C&#xff0c;usb 等。如下图所示&#xff1a; 本章我们需要重点关注下 class 目录&#xff0c;这个目录下放置的是一…

MyBatis-Plus 进阶之条件构造器Wrapper和常用接口

目录 1.wrapper 介绍 1.1什么是 wrapper 2.QueryWrapper 2.1测试前的准备 2.2例1&#xff1a;组装查询条件 2.3例2&#xff1a;组装排序条件 2.4例3&#xff1a;组装删除条件 2.5例4&#xff1a;条件的优先级 2.6例5&#xff1a;组装 select 子句 2.7例6&#xff1a;…

Redis常见的数据类型和应用场景

目录 Redis概述 简介 特点 架构 使用场景 Mermaid图示 Redis数据类型 基础数据类型 扩展数据类型&#xff08;Redis 3.2及以上版本&#xff09; Mermaid图示 String类型详解 定义 内部实现 应用场景 Mermaid图示 List类型详解 定义 内部实现 应用场景 Merma…

旧数据与新系统 —— 重现数据迁移之旅

某天&#xff0c;正做卡呢。突然收到客户通知&#xff1a;不用做了&#xff0c;这系统不要了。 啊? 都搞了好几年&#xff0c;说不要就不要了&#xff0c;客户这么财大气粗&#xff1f; 细问之下&#xff0c;原来变化来源于最新的商业决策——客户收购了一个市场占有率比较大…

基于JSP的社区疫情防控管理信息系统

你好&#xff0c;我是专注于计算机技术研究的学姐。如果你对社区疫情防控管理信息系统感兴趣或有相关需求&#xff0c;欢迎私信交流。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSPJavaBeansServlet 工具&#xff1a;MyEclipse, Tomcat 系统展示…

仅12%程序员担心被AI取代 62%开发者在使用AI工具

**根据Stack Overflow近日发布的2024年开发者调查报告&#xff0c;只有12%的开发者认为AI威胁到了他们当前的工作&#xff0c;而高达70%的受访者已经将AI工具整合到了自己的工作流程中。**该调查共有超过6.5万名开发者参与&#xff0c;结果显示&#xff0c;使用AI工具的开发者比…

USBCAN-II/II+使用方法以及qt操作介绍

一.USBCAN-II/II介绍 USBCAN-II/II 是一款常用的 USB-CAN 转换器&#xff0c;广泛应用于汽车电子、工业自动化等领域。以下是使用该设备的一般步骤和方法&#xff1a; 1. 硬件连接 连接设备&#xff1a;将 USBCAN-II/II 的 USB 接口连接到计算机的 USB 端口。 连接 CAN 网络…

多功能声学气膜馆:解决城市噪音难题的标杆建筑—轻空间

在现代城市中&#xff0c;噪音污染已成为一个普遍存在且难以解决的问题。随着城市人口的增加和交通的繁忙&#xff0c;如何为市民提供一个安静、舒适的活动空间&#xff0c;成为城市规划者和建筑设计师亟待解决的挑战。多功能声学气膜馆&#xff0c;作为一种创新的建筑形式&…

只有IP如何实现https访问

IP也是访问网站的一种方式&#xff0c;现在有很多网站并未绑定域名&#xff0c;而是通过IP直接访问的。 但是域名访问网站的方式会更多一些&#xff0c;主要还是因为域名相较于IP数字要更加好记&#xff0c;所以域名绑定网站的情况会更多。 随着现在网络安全意识的逐渐提升&a…

Android MediaRecorder 视频录制及报错解决

目录 一、start failed: -19 二、使用MediaRecorder录制视频 2.1 申请权限 2.2 布局文件 2.3 MediaRecordActivity 2.4 运行结果 三、拓展 3.1 录制视频模糊(解决) 3.2 阿里云OSS上传文件 3.2.1 权限(刚需) 3.2.2 安装SDK 3.2.3 使用 相关链接 一、start failed…

[每周一更]-(第110期):QT开发最佳实战(php/go/python/javascript)

文章目录 1. 使用 Go 开发 Qt 应用工具&#xff1a;therecipe/qt安装和配置示例代码 2. 使用 Python 开发 Qt 应用工具&#xff1a;PyQt 或 PySide安装和配置示例代码&#xff08;PyQt5&#xff09; 3. 使用 PHP 开发 Qt 应用工具&#xff1a;PHP-Qt安装和配置示例代码 4. 使用…

VLL基本原理

VLL的基本架构 VLL技术通过隧道承载CE&#xff08;Customer Edge&#xff09;端的各种二层业务&#xff0c;透明传递CE端的二层数据&#xff0c;为用户提供点对点的二层VPN服务。 VLL的基本架构可以分为AC、VC和Tunnel三个部分&#xff0c;而PW这个概念也会经常用到。如图1所…

工控接入网关:实现工业物联网的关键技术

工业控制系统随着科技的发展&#xff0c;逐渐向数字化、智能化的方向发展。工控接入网关作为工业物联网的关键技术之一&#xff0c;具有重要的作用和意义。本文将从工控接入网关的定义、功能、应用、优势等方面进行全面详细地介绍。    一、工控接入网关的定义与功能 工控接…

新零售社交电商系统前景分析

新零售社交电商系统前景分析 新零售社交电商系统作为数字经济的创新趋势之一&#xff0c;展现出强大的市场活力和发展潜力。 以下是对其前景的分析&#xff1a; 市场规模的增长&#xff1a;中国社交电商的交易规模在2021年达到了23785.7亿元&#xff0c;并预计在2023年将增长…

面了拼多多算法岗,被疯狂拷打。。。

暑期实习基本结束了&#xff0c;校招即将开启。 不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球友解惑答疑&…

8-4 循环神经网络

对于 (8.4.2)中的函数 f f f&#xff0c;隐变量模型不是近似值。 毕竟 h t h_{t} ht​是可以仅仅存储到目前为止观察到的所有数据&#xff0c; 然而这样的操作可能会使计算和存储的代价都变得昂贵。 回想一下&#xff0c;我们在前面讨论过的具有隐藏单元的隐藏层。 值得注意的…

VS Code安装配置ssh服务结合内网穿透远程连接本地服务器详细步骤

文章目录 前言1. 安装OpenSSH2.VS Code配置ssh3. 局域网测试连接远程服务器4. 公网远程连接4.1 ubuntu安装cpolar内网穿透4.2 创建隧道映射4.3 测试公网远程连接 5. 配置固定TCP端口地址5.1 保留一个固定TCP端口地址5.2 配置固定TCP端口地址5.3 测试固定公网地址远程 前言 远程…

Windows系统开机出现Minimal BASH-like line editingis supported解决方法

Minimal BASH-like line editingis supported解决方法 一、问题描述 因为电脑不知道是不是安装了双系统&#xff0c;开机的时候会出现标题中的这个问题&#xff0c;网上查找资料一般说是双系统的问题&#xff0c;引导项缺失&#xff0c;然后Ubuntu进不去。可是我的电脑好像没有…

LR12 在Controller设置脚本并运行

1、当在Generator配置脚本完成后&#xff0c;点击工具-创建Controller场景&#xff0c;输入线程数即Vuser后点击确定。 2、这时会Controller中打开刚才配置的脚本&#xff0c;显示如下图所示&#xff0c; 在全局计划中点击启动Vuser,界面会显示如下弹窗&#xff0c;设置并发用户…

NTP时间服务器的工作原理解析

在当今数字化和信息化高度发展的时代&#xff0c;时间的准确性和同步性对于各种系统和应用程序至关重要。NTP时间服务器作为确保时间精准同步的关键设备&#xff0c;发挥着不可或缺的作用。 NTP 时间服务器的核心目标是为网络中的设备提供准确、一致和可靠的时间信息。它基于一…