canvas绘制奔跑的小恐龙动画与时钟案例讲解!

504 阅读6分钟

大家好,我是潘潘。

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

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

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

canvas绘制动画

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

绘制原理

清屏→更新→渲染

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

常用的绘制方法

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

  1. setTimeout(code, milliseconds, param1, param2, ...); :延时器,不多讲;

  2. setInterval(function, milliseconds, param1, param2, ...); :定时器,不多讲;

  3. window.requestAnimationFrame(callback) :告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

Snipaste_2022-10-17_11-41-12.png

`setTimeout/ 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>

绘制结果:

小恐龙.gif

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

绘制钟表

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

i

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

绘制钟表同样是遵循清屏→更新→渲染的原理,不过这里我们使用的是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>

绘制结果:

时钟动画.gif

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

重绘小恐龙

我封装了一下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>

显示:

小恐龙.gif

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

GIF 2022-10-15 19-00-11.gif

欢迎大家关注我的个人公众号:学编程的GISer(微信号dapancoder),获取关于canvas更多的干货知识