zl程序教程

您现在的位置是:首页 >  Java

当前栏目

canvas绘制动画原理及案例讲解(绘制小恐龙动画、时钟等)

2023-02-18 16:38:32 时间

大家好,我是潘潘。

这期为大家带来的是canvas的动画绘制案例与讲解。不知道大家上一期canvas绘制基本图形的入门教程看的怎么样了,如果已经遗忘了或者还没看的小伙伴建议先去看一下,上一期是学习这一期的基础:

canvas详细教程!(近1万字吐血分享)

因为canvas的功能实在太强大了,为了让大家一点一点来,这里只展示了几个适合新手学习的canvas绘制动画的案例,高级动画案例会在下一期讲解

canvas绘制动画

在绘制动画之前,我们先了解一下canvas绘制动画的基本原理和方法。

绘制原理

清屏→更新→渲染

在canvas之前,在web端绘制动画都是用Flash实现的,但是Flash漏洞很多,还必须安装插件(记不记得小时候玩一些小游戏和播放视频时提示要下载flash插件),Flash在2021年初已经被正式停用了。canvas的出现颠覆了Flash的地位,无论是广告、游戏都可以用canvas实现,Canvas是一个轻量级的画布,在使用canvas绘制的时候,一旦绘制成功,就会像素化它们,canvas没有再次从画布上得到这个图形的能力,没有能力再去修改已经画在画布上的内容,这也是canvas比较轻量的原因。所以,如果要在同一地方绘制不同的图案,就需要先清除画布的这一区域,再绘制新图案。

常用的绘制方法

canvas上绘制内容是要在js脚本执行结束之后才能看到结果,所以我们不能在for循环中完成动画的绘制,而是常用一些浏览器内置的方法:

  1. setTimeout(code, milliseconds, param1, param2, ...); :延时器,不多讲;
  2. setInterval(function, milliseconds, param1, param2, ...); :定时器,不多讲;
  3. window.requestAnimationFrame(callback) :告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

MDNsetTimeout/ setInterval 的显著缺陷就是设定的时间并不精确,它们只是在设定的时间后将相应任务添加到任务队列中,而任务队列中如果还有前面的任务尚未执行完毕,那么后添加的任务就必须等待,这个等待的时间造成了原本设定的动画时间间隔不准requestAnimationFrame的到来就是解决这个问题的 ,requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。 设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。 requestAnimationFrame的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。 不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。 requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。

在搞懂了canvas绘制动画的原理和方法,我们来绘制几个动画:

奔跑的小恐龙

这个动画的原理很简单,就是使用setInterval()方法不断地添加渲染的图片(这里不需要清屏步骤,因为我们直接绘制新的图片覆盖了旧图片),让图片连贯起来,看起来像是动图。上代码:

<canvas id="canvas" height="600" width="700"></canvas>
<script>
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    // 存储图片的是src:
    const imgSrcs = ['http://panpan.dapanna.cn//image-20221015115049427.png', 'http://panpan.dapanna.cn//image-20221015115033342.png', 'http://panpan.dapanna.cn//image-20221015115015133.png', 'http://panpan.dapanna.cn//image-20221015114950581.png', 'http://panpan.dapanna.cn//image-20221015114245445.png', 'http://panpan.dapanna.cn//image-20221015114437817.png', 'http://panpan.dapanna.cn//image-20221015114526684.png', 'http://panpan.dapanna.cn//image-20221015114610049.png', 'http://panpan.dapanna.cn//image-20221015114653366.png', 'http://panpan.dapanna.cn//image-20221015114722067.png', 'http://panpan.dapanna.cn//image-20221015114802665.png', 'http://panpan.dapanna.cn//image-20221015114927924.png']
    const img = new Image()
    var i = 0
    // 间隔70ms绘制一次图片,:
    setInterval(() => {
        img.src = imgSrcs[i]
        img.onload = () => {
            ctx.drawImage(img, 60, 120) // 绘制图片,这里看不懂的小伙伴建议去看我上一期写的canvas基础教程
        }
        i++
        if (i === 12) { 
            i = 0
        }
    }, 70)
</script>

绘制结果:

小恐龙

有的小伙伴可能会问,既然在前边讲了那么多setInterval()方法的缺点和requestAnimationFrame()方法的优势,为什么在这里绘制动画还要使用setInterval()方法呢?别急,等下我们会使用requestAnimationFrame()方法重新写一遍这个动画。

绘制钟表

如果你去浏览器百度“时间”两个字,你会发现网页上的时钟就是拿canvas写的:

那么我们也来尝试一下画一个时钟吧!

绘制钟表同样是遵循清屏→更新→渲染的原理,不过这里我们使用的是requestAnimationFrame()方法,大致思路就是使用requestAnimationFrame方法不断获取当前的时间,包括时、分、秒,并且根据获取的时间,结合时钟的‘针’所应旋转的角度,不断地清屏和重绘即可。详细思路直接看代码中的注释:

<canvas id="canvas" height="800" width="900"></canvas>
<script>
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    // 绘制时钟显示之前的文本提示:
    ctx.font = '50px s'
    ctx.textAlign = 'center'
    ctx.strokeText('你即将看到时钟', 450, 400, 400)
    // 绘制时钟:
    function draw() {
        // 获取当前时间:
        const date = new Date()
        // 获取当前秒:
        let second = date.getSeconds()
        // 获取当前分:
        let minutes = date.getMinutes()
        // 获取当前时:
        let hour = date.getHours()
        // 每次循环都要线清空画布
        ctx.clearRect(0, 0, canvas.width, canvas.height)
        ctx.save() // 保存状态1
        ctx.translate(450, 400) // 移动画布原点
        // 绘制时间刻度:
        for (i = 0; i < 60; i++) {
            ctx.save() // 保存状态2
            ctx.beginPath()
            ctx.rotate([(Math.PI) / 180] * 6 * i)
            ctx.moveTo(0, -400)
            ctx.lineTo(0, -380)
            // 当刻度为5的整数倍的时候,加粗:
            if (i % 5 == 0) {
                // 绘制时钟上的时间刻度:
                ctx.save() // 保存状态3
                ctx.translate(0, -350)
                ctx.rotate([-(Math.PI / 180)] * 6 * i)
                ctx.font = '30px s'
                ctx.textAlign = 'center'
                ctx.textBaseline = 'middle'
                ctx.fillText(`${i / 5 == 0 ? 12 : i / 5}`, 0, 0, 50) // 绘制出1-12刻度文字
                ctx.restore() // 恢复状态3
                // 让时间刻度为5的倍数的刻度加粗:
                ctx.lineWidth = 5

            }
            ctx.stroke()
            ctx.restore() // 恢复状态2
        }
        ctx.restore() // 恢复状态1
        ctx.save() // 保存状态4
        ctx.save() // 保存状态5
        ctx.save() // 保存状态6
        // 绘制时分秒针交点地方的小黑圆:
        ctx.beginPath()
        ctx.arc(450, 400, 400, 0, [(Math.PI) / 180] * 360)
        ctx.stroke()
        ctx.beginPath()
        ctx.arc(450, 400, 5, 0, [(Math.PI) / 180] * 360)
        ctx.fill()
        // 画秒针:
        ctx.beginPath()
        ctx.translate(450, 400)
        ctx.rotate([(Math.PI) / 180] * second * 6) // 换算秒针的旋转角度
        ctx.moveTo(0, 0)
        ctx.lineTo(0, -320)
        ctx.strokeStyle = 'red'
        ctx.stroke()
        // 画时针:
        ctx.restore() // 恢复状态6
        ctx.beginPath()
        ctx.translate(450, 400)
        ctx.rotate([hour * (Math.PI) / 180] * 3600 * 1 / 120) // 换算秒时针的旋转角度
        ctx.rotate([minutes * (Math.PI) / 180] * 1 / 2) // 换算秒时针的旋转角度
        ctx.rotate([(Math.PI) / 180] * second * 1 / 120) // 换算秒时针的旋转角度
        ctx.moveTo(0, 0)
        ctx.lineTo(0, -100)
        ctx.lineWidth = 8
        ctx.stroke()
        // 画分针:
        ctx.restore() // 恢复状态5
        ctx.beginPath()
        ctx.translate(450, 400)
        ctx.rotate((Math.PI) * 2 * minutes / 60) // 换算分针的旋转角度
        ctx.rotate([(Math.PI) / 180] * second * 1 / 10) // 换算分针的旋转角度
        ctx.moveTo(0, 0)
        ctx.lineTo(0, -240)
        ctx.lineWidth = 4
        ctx.strokeStyle = 'blue'
        ctx.stroke()
        ctx.beginPath()
        ctx.restore() // 恢复状态4
        window.requestAnimationFrame(draw)
    }
    window.requestAnimationFrame(draw)
</script>

绘制结果:

时钟

⬆为了便于大家观看,具体步骤我写在了代码块的注释中

重绘小恐龙

我封装了一下requestAnimationFrame()方法,这样我们既可以用到requestAnimationFrame方法的优点,又可以自由控制每次调用绘制函数的时间间隔:

封装:

// 重新封装requestAnimationFrame函数:
function mySetInterval(func, detay) {
    var i = 0
    myReq = requestAnimationFrame(function fn() {
        // 判断现在处于60帧的第几帧,如果是目标帧的话,调用func函数:
        if (i % parseInt(60 / (1000 / detay)) == 0) {
            func();
        }
        i++
        // 让i值每秒增加60,循环调用func函数:
        requestAnimationFrame(fn)
    })
}

// 调用封装好的函数,一秒钟打印一次'111':
mySetInterval(function () {
    console.log(111);
}, 1000)

这样我们就可以调用封装的mySetInterval方法来代替setInterval方法了:

<canvas id="canvas" height="600" width="700"></canvas>
<script>
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')

    // 存储图片的链接:
    const imgSrcs = ['http://panpan.dapanna.cn//image-20221015115049427.png', 'http://panpan.dapanna.cn//image-20221015115033342.png', 'http://panpan.dapanna.cn//image-20221015115015133.png', 'http://panpan.dapanna.cn//image-20221015114950581.png', 'http://panpan.dapanna.cn//image-20221015114245445.png', 'http://panpan.dapanna.cn//image-20221015114437817.png', 'http://panpan.dapanna.cn//image-20221015114526684.png', 'http://panpan.dapanna.cn//image-20221015114610049.png', 'http://panpan.dapanna.cn//image-20221015114653366.png', 'http://panpan.dapanna.cn//image-20221015114722067.png', 'http://panpan.dapanna.cn//image-20221015114802665.png', 'http://panpan.dapanna.cn//image-20221015114927924.png']
    const img = new Image()
    var i = 0

    // 重新封装requestAnimationFrame函数:
    function mySetInterval(func, detay) {
        var i = 0
        myReq = requestAnimationFrame(function fn() {
            // 判断现在处于60帧的第几帧,如果是目标帧的话,调用func函数:
            if (i % parseInt(60 / (1000 / detay)) == 0) {
                func();
            }
            i++
            // 让i值每秒增加60,循环调用func函数:
            requestAnimationFrame(fn)
        })
    }

    // 不断绘制新的图片:
    mySetInterval(() => {
        img.src = imgSrcs[i]
        img.onload = () => {
            ctx.drawImage(img, 60, 120)
        }
        i++
        if (i === 12) {
            i = 0
        }
    }, 70)
</script>

显示:

小恐龙

以上就是canvas绘制基本动画的案例,高级动画(添加上物理效果,如下图⬇)的讲解会在下一期,有兴趣的小伙伴可以关注我,不定期发一些对你有用或好玩的干货内容!

小球物理