H5序列帧动画实现过程(附源码)

H5序列帧动画实现过程(附源码)

序列帧动画

序列帧动画,又称为逐帧动画,是使用多张连续的静态图片快速切换实现视频动画效果的一种技术。在一些移动设备上展现视频动画,如果使用video标签(或其他播放视频的方法),会因为设备性能不足、预加载不完全等问题导致视频卡顿。某些应用场景下,需要使用视频动画作为场景背景时,上述的卡顿情况更加严重。当然,可以使用GIF图片作为动态背景图,但如果需要控制视频循环次数、与背景音乐同步时,GIF又无能为力了。

所以,序列帧动画利用Canvas强大的重绘能力、Image对象的预加载能解决了上述的性能问题、预加载问题。同时,图片切换非常容易控制间隔时间和次数,可以完美与背景音乐匹配。

示例

在线示例请点击这里,本地示例请点击下载后打开demo.html。

接口

新建对象

frame_ani(option);

接受一个字典作为输入,修改该对象的初始值。option的默认值如下:

this.option = {
    canvasTargetId: null, // 目标画布对象ID(必须)
    framesUrl : [],       // 每一帧的url(必须)

    audioObject: null,    // 音频的对象(优先级高于ION)
    audioIonName: null,   // ION音频的名字(优先级高于路径)
    audioUrl: "",         // 音频路径(优先级最低)

    height: 300,          // 图片的高度(必须)
    width: 300,           // 图片的宽度(必须)

    onStart : null,       // 加载开始回调函数,传入参数total
    onComplete : null,    // 播放完毕回调

    loop: false,          // 是否循环
    frequency: 25,        // 每秒帧数
}

初始化序列帧动画

frame_ani.initialize(callback);

初始化包括图片预加载和封面绘制。该函数不返回值,接受一个函数作为回调函数,会在初始化结束后被调用。

播放控制

frame_ani.play();  // 播放
frame_ani.pause();  // 暂停
frame_ani.reset();  // 重置

实现逻辑细节

实现的原理比较简单:
预加载所有序列图——选择一个Canvas容器——将一张图片画上去——等待间隔时间——将下一帧图片画上去
画完最后一张图,循环计数一次,决定是否要循环下一次

这样就可以完成核心的序列帧动画的播放功能,背景音乐只需要在视频播放开始和结束处理一下就行。
接下来我将整理一下使用到的各个关键技术点:

图片的预加载

在JS语言中,给Image对象的src属性赋值一个图片的URL,就会启动该图片的加载。加载成功会调用Image.onload函数,加载失败会调用Image.onerror函数。于是,把这两个函数指向我们的计数函数,就可以得知一个Image对象是否加载完毕。

如果要同时加载一批图片,就新建等量的Image对象,将他们的Image.onload函数和Image.onerror函数都赋值为一个计数函数,然后分别给每个Image对象赋值对应的图片URL。计数函数统计加载完毕的数量,如果等于加载总量,就说明这一批图片都加载完成了。

源码中,预加载部分主要由initialize(callback)函数启动,由loaded()函数进行计数。所有帧图片加载完毕后,会调用setPoster()函数绘制第一帧为封面,该函数也是一个绘制Canvas的最小例子,简洁易懂。

图片预加载部分主要参考这篇文章,感谢。

获取Canvas对象上下文

var ctx = document.getElementById(canvas_DOM_id).getContext('2d');

通过上述代码可以获取到Canvas对象的上下文,通过调用上下文ctx的函数,可以实现Canvas画布的绘制、擦除等操作。

注意,对于一个Canvas标签<canvas>来说,它本身有Height、Width属性,是用来决定画布的分辨率的,如果不主动设置,那么就是150*150的默认值。同时,CSS规则也可以作用于<canvas>,但只会修改它在页面上的高宽,不会修改其分辨率。如果没有设置CSS中的样式高宽,那么就以<canvas>上的高宽属性显示。所以,在实际应用时不能将这两组Height、Width混为一谈。

图片的绘制时机

浏览器提供了window.requestAnimationFrame(callback)函数用于显示动画效果。该函数接受一个函数作为参数,调用这个函数后,下一次浏览器要刷新页面之前,就会调用它的callback函数,并将一个时间戳作为参数传给这个callback。那么,在浏览器刷新页面时,新的内容就已经准备好了。

在序列帧应用中,可以简单理解为每次要绘制下一帧时,就调用window.requestAnimationFrame(callback)函数,并将绘制Canvas的函数作为参数传进去。

在绘制Canvas的函数中,通过处理时间戳,可以用来控制视频播放的快慢(大概来说就是间隔时间太短就不重绘、间隔太长就跳过几帧去绘制)。

处理完时间戳后,如果要进行重绘,就执行以下三个步骤:计算绘制位置(可选)——清空画布——绘制Image对象到Canvas容器中。

完成了一次绘制后,将当前帧数+1,如果小于最大帧数,就继续调用window.requestAnimationFrame(callback)函数绘制下一帧。如果已经等于最大帧数了,就说明视频放完一轮了,可以调用一下自己的onComplete回调函数,处理一下背景音乐,决定一下是否要loop。这些都是琐碎的细节,不展开讲了。

用于传给window.requestAnimationFrame(callback)函数的绘制函数对应源码中的nextFrame(timestamp)函数。

图片的绘制位置

在一个Canvas容器上绘制图片,使用的是ctx对象的drawImage函数。该函数有好三种用法,这里用的是最多参数的一个。函数如下:

void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

函数的第一个参数image是一个Image对象。sx, sy, sWidth, sHeight四个参数决定了源图片(Source)要展示的区域;dx, dy, dWidth, dHeight四个参数决定在目标容器(Destination,即Canvas画布)上,这个源图片要绘制的区域。示意图如下:

drawImage

详细的函数讲解请点击这里,上图也是引用自这篇文档,本文不细讲这个函数。

在序列帧动画实现的过程中,每次绘制图片时,采用的方式不是固定的。就好比在CSS中使用background-position一样,根据应用场景的需要来决定使用哪种对齐方式。在我的代码实现中,采用的是类似background-position: center bottom;,显示长边的绘制方式。

用人话来说:图片在画布上水平居中,竖直贴底显示。保证源图片的长宽比不变,多余的部分将被裁去,主要显示长边,保证Canvas容器被填满。

这个部分似乎用人话也讲不清楚,各位可以在示例上改改Canvas容器的宽高比就清楚了。再次重申一次,如何决定剪裁、绘制的位置是不固定的,应当根据应用需要来决定。这部分在源码中,由calculate函数来计算全局的剪裁、绘制位置。在nextFrame函数中有被注释掉的一句,在每次计算重绘的时候先调用calculate函数计算一波。后来觉得这样写不优美,也没想到其他方法来应对canvas容器宽高动态变化,就暂时先注释掉了。

建议:在使用这个库的时候,保证图片的宽高比和Canvas容器的宽高比相同,再将<canvas>放到一个<div>中,用CSS来解决剪裁(或称遮挡)的问题是比较合理的。

背景音乐的处理

这一部分不是序列帧动画的重点,只是用序列帧实现动画的附加产物。作为背景音乐,只要能保证视频和音频大致同步就行。老师傅多年的经验告诉我,每秒播放25帧左右,就能和背景音乐同步。当然,这个帧数也与视频导出时的帧数有关,具体来说要在实践中尝试出来。

背景音乐的播放,只是原生的<audio>,也支持Ion.sound库来播放。在移动设备上,ion.sound可以保证音频的预加载和流畅播放,非常推荐。

在代码中,this.bgm变量保存着背景音乐,在视频的播放、暂停、停止、循环开始时,都要进行对应的处理。这些细节也比较琐碎,就不赘述了。

源码

源码托管于Github:点击这里
遵循MIT协议。


  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值