前言
作为前端同学,或多或少都会接到动画需求。如果是有规律性的动画还是相对容易实现的,但如果是比较复杂的帧动画,我们用 CSS 实现的话,就非常容易造成如下情况,设计师是卖家秀,我们开发的是买家秀。
或许你会想到用 GIF 实现,但是 GIF 经常会有杂边,无法满足设计师对精致度的要求。所以我们需要寻找更多的动画方案,能够让我们 100% 还原设计稿,又保证动画的精致度和性能。本文笔者主要是介绍的是 APNG 方案。
APNG(Animated Portable Network Graphics)是基于 PNG 格式扩展的一种动画格式,增加了对动画图像的支持,同时加入了 24 位图像和 8 位 Alpha 透明度的支持,这意味着动画将拥有更好的质量。
首先来看下 APNG 和 GIF 的对比效果:
上面的图不动的话,或者查看更多 Demo 请直接看 Demo1 和 Demo2,可以发现 APNG 和 GIF 的大小虽然相差不大,但是 APNG 要比 GIF 清晰的多,并且没有杂边。这是因为 APNG 拥有 24 位图像和 8 位 Alpha 透明度的支持。接下来一起看看 APNG 的主要原理和使用吧。
1. APNG 数据格式
1.1 PNG
在查看 APNG 数据格式前,要先了解下 PNG 的数据格式,毕竟 APNG 是基于 PNG 格式扩展的。PNG 的数据格式如下:
主要分为 4 部分:
-
PNG Signature 是文件标识,用于校验文件格式是否为 PNG。内容固定为:
-
IHDR 是文件头数据块,包含 PNG 图像的基本信息,例如图像的宽高等信息
-
IDAT 是图像数据块,最核心,存储具体的图像数据
-
IEND 是结束数据块,标示图像结束
1.2 APNG
在了解 PNG 的数据格式后,再来看下 APNG 的数据格式。如下图所示:
可以看到,APNG 在 PNG 的基础上增加了 acTL、fcTL 和 fdAT 3 种模块
-
acTL:必须在第一个 IDAT 块之前,用于告诉解析器这是一个动画格式的 PNG,包含动画帧总数和循环次数的信息,意味着可以通过这个字段来判断是否为 APNG 的图像格式。
-
fcTL:帧控制块,每一帧都必须有的,属于 PNG 规范中的辅助块,包含了当前帧的序列号、图像的宽高。
-
fdAT:帧数据块,和 IDAT 意义相同,都是图像数据。但是比 IDAT 多了帧的序列号,因为动画存在多帧。图中可以看到第一帧的图像数据依然叫做 IDAT,第 2 帧以后才叫 fdAT,这是因为第一帧和 PNG 数据的格式保持相同。在不支持 APNG 的浏览器上,可以降级为静态图片,只展示第一帧。
为了更好的理解 APNG 数据格式,感兴趣的同学可以通过下方 APNGb 这个软件,自己生成 APNG 动画。下面的 DEMO 是用 4 张时钟图片生成。
效果:(不动的话就直接看上述 Demo)
2. 性能
学习完 APNG 的数据格式,以及通过上面的 Demo 我们可以发现,一个时钟动画存储了 4 帧时钟图像数据,意味着一张 APNG 动画必然很大。如果有几十帧,那更是不敢想象了。页面加载动画很慢,反而造成不好的用户体验,那动画也没什么存在的意义了。
但是 APNG 的团队也意识到这个问题,因此也会进行帧优化:
如上这 4 帧,可以看出表盘部分是可以复用的,因此在生成 APNG 前,APNG 会通过算法计算帧之间的差异,只存储帧之前的差异,而不是存储全帧。如下,第 2、3、4 帧都没有表盘部分了。
优化后的 APNG 大小如下,可以看出第 2、3、4 帧数据要比第一帧小了很多。
但是这里有个问题便是,第 2、3、4 帧如何绘制呢?如何知道复用哪些元素呢?这个问题会在后面解答。
3. apng-canvas 源码分析
平时我们使用 APNG 方式如下,非常简单:
但是直接使用 img
标签存在 2 个问题:
- 兼容性问题,APNG 兼容性目前来看还算可以,取决于各个公司希望的兼容程度使用。
- 一个非常大的坑,在 Safari for iOS(Safari for macOS正常)预览 APNG 的时候,动图的循环次数为对应原图的
loop
+ 1。比如 APNG 有 10帧,loop
为 2,那么会循环总计展示 30 帧。如果我们的动画只想播放一次的话,那就糟糕了。
所以一般我们推荐 使用 apng-canvas 这个库。该库需要以下支持才能运行:
- Canvas
- Typed Arrays
- Blob URLs
- requestAnimationFrame
接下来带大家看一下 apng-canvas 库是如何实现 APNG 正常播放的,主要分为 3 个步骤:
- 解析 APNG 数据格式(按照 1.2 小节的 APNG 图片格式)。
- 将解析好的 APNG 数据进行整理。
- 按照每一帧的间隔时长,通过 requestAnimationFrame 进行绘制每一帧。
源码 apng-canvas/src 目录结构如下:
3.1 解析 APNG 数据格式
解码流程如下:
APNG 的文件加载是通过 XMLHttpRequest
下载,可以看下 /src/loader.js ,不做解释。
解码逻辑主要是在 /src/parser.js 中,首先将 APNG 以 arraybuffer
的格式下载资源,通过操作二进制数据,校验文件格式是否为 PNG & APNG。
校验 PNG 格式就是校验 PNG Signature
块,在 1.1小节 PNG 数据格式中已提到,关键实现如下:
校验 APNG 格式就是判断文件是否存在类型为 acTL
的块,在 1.2小节 APNG 数据格式中已提到。依序读取文件中的每一块,获取块类型等数据,判断代码如下:
解码并整理每一帧数据的过程,如下代码所示。调用 parseChunks
依次读取每一块,根据每种类型块中包含的数据、宽高、对应的位置、字节大小分别进行处理存储。
上面将每一帧图像的宽高、位置、播放时长等处理好后,将每一帧的帧数据 dataParts
按序组成一份 PNG 图像资源,通过 createObjectURL
创建图片的 URL 存储到frame中,用于后续绘制。这里代码省略,感兴趣自行查看源码。
解码这一块比较无聊,想了解更详细的可以看@网易云音乐专栏的这篇文章哈~ APNG 解码 ,笔者主要是带大家理清思路即可。
3.2 整理解析好的 APNG 数据
从 3.1小节可以看出将解析出的数据依次存储到 anim.frames
中了,前面提到的时钟案例解析结果如下:
上述 4 帧数据,分别对应下面 4 张图,前面提到过这是优化后的效果:
也可以看出只有第一帧的 width
、 height
、 left
、 top
比较完整,第 2、3、4 帧的 width
、 height
、 left
、 top
都是不同的,因为被算法优化过。
那么 blendOp
和 disposeOp
字段分别代表什么呢?可以看出笔者是没有注释的,这 2 个字段就是前文第 2 节中提到的 【第 2、3、4 帧如何绘制呢?如何知道复用哪些元素呢?】问题的答案。那具体如何处理的,在下一节绘制中再来解答。
3.3 绘制每一帧
APNG 的绘制,主要是通过 requestAnimationFrame
不断的调用 renderFrame
方法绘制每一帧,每一帧的图像、宽高、位置我们都在上一节中获取到了。 requestAnimationFrame
在正常情况下能达到 60 fps(每隔 16.7ms 左右),在上一节中提过 playTime
这个字段,是每一帧的绘制时间。所以,并不是 requestAnimationFrame
每次都会去绘制,而是通过 playTime
计算 nextRenderTime
(下次绘制时间),达到这个时间再绘制。避免无用的绘制,对性能造成影响。代码如下:
具体的绘制是采用 Canvas 2D 的 API 实现。
从上面的绘制代码中,我们可以看到 blendOp
和 disposeOp
2个字段决定了是否复用绘制过的帧数据。2 个字段对应的配置参数信息如下:
disposeOp
指定了下一帧绘制之前对缓冲区的操作- 0:不清空画布,直接把新的图像数据渲染到画布指定的区域
- 1:在渲染下一帧前将当前帧的区域内的画布清空为默认背景色
- 2:在渲染下一帧前将画布的当前帧区域内恢复为上一帧绘制后的结果
blendOp
指定了绘制当前帧之前对缓冲区的操作- 0:表示清除当前区域再绘制
- 1:表示不清除直接绘制当前区域,图像叠加
对应时钟 4 帧绘制流程如下:
-
第一帧:
- blendOp:0 绘制当前帧之前,清除当前区域再绘制
- disposeOp:0 不清空画布,直接把新的图像数据渲染到画布指定的区域
-
第二帧:
- blendOp:1 绘制当前帧之前,表示不清除直接绘制当前区域,图像叠加
- disposeOp:0 不清空画布,直接把新的图像数据渲染到画布指定的区域
-
第三帧:
- blendOp:1 绘制当前帧之前,表示不清除直接绘制当前区域,图像叠加
- disposeOp:2 渲染下一帧前将画布的当前帧区域内恢复为上一帧绘制后的结果(因为第4张图覆盖的是第二张图的红色线条,所以第三张图动完要回到第2帧)
-
第四帧:
- blendOp:1 绘制当前帧之前,表示不清除直接绘制当前区域,图像叠加
- disposeOp:0 不清空画布,直接把新的图像数据渲染到画布指定的区域
至此 apng-canvas 的绘制流程便讲完了,感兴趣的同学可以源码多琢磨下~
4. APNG 兼容性检测
在实际应用中如何检测浏览器是够支持 APNG,可以通过如下方法:
-
加载一张 1x1 像素大小的 Base64 编码图片,图像有 2 帧数据,区分就是每一帧最后一个值不同。
-
将其绘制到画布中,通过 getImageData() 方法去获取该图片的像素数据,主要是获取
data[3]
的 Alpha 透明通道(值的范围:0 - 255)。在不支持 APNG 的浏览器上会降级只显示第一帧,因此data[3]
会等于 255。在支持 APNG 的浏览器上最终会显示第 2 帧,因此data[3]
会等于 0,则表示支持 APNG。
5. 总结
-
本文介绍了 APNG 的使用、性能、踩坑、兼容性以及检测、 apng-canvas 库源码分析,主要是对笔者个人学习进行总结。
-
在实际使用中,由于 Safari for iOS 中
loop
会自动+1
,所以不适合那些只播放一次的动画。 -
APNG 文件存储多帧数据会很大,所以建议使用比较小的动画场景上。如果场景合适,也可以放一张静态图在底部,待 APNG 加载完毕后替换,不过这种需要第一帧是可以对用户静态展示的。
-
apng-canvas 解码比较耗时,如果动画是进页面就展示的,会增加页面阻塞时间。笔者尝试过放到Web Worker 中解析,可以节省耗时 100ms 左右。
6. 参考资料
文中图片和相关信息来自以下参考资料:
- gist.github.com/eligrey/175…
- wiki.mozilla.org/APNG_Specif…
- littlesvr.ca/apng/inter-…
- github.com/davidmz/apn…
- Web 端 APNG 播放实现原理
- APNG 那些事
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!