apng逐渐成为大部分业务实现复杂动效、动画的方案。这种方案有下面几个优势:html
咱们的智能辅播业务也有这样的使用场景。以下图前端
图片可能会被降级点击查看: https://gw.alicdn.com/imgextr...
上面这张图在设计师经过软件制做出来时,是一个无限循环的apng文件。因此不加处理直接展现在设备上时将会循环播放。而下面这幅图在设计出来就是一个播放1次的动画(若是没看到动做能够直接复制图片连接在浏览器打开。git
图片可能会被降级,点击查看: https://gw.alicdn.com/imgextr...
一个良好的网页应该遵循基本的规范,比如W3C无障碍规范中明确的:github
不要设计会致使癫痫发做或身体反应的内容。web
网页不包含任何闪光超过3次/秒的内容,或闪光低于通常闪光和红色闪光阈值。.typescript
因此页面上的动画不该该一直重复播放(一方面会夺了用户的焦点,另外一方面使人烦躁)。在智能辅播的业务中,咱们规定了动画只在获取到小助理新的对话内容的时候才播放一次。segmentfault
在weex环境下,咱们的设计师直接产出一个不循环播放的apng文件,前端只须要加载便可。在h5环境下,其实咱们能直接控制apng的播放。数组
apng-canvas 是一个用于在浏览器环境下控制apng文件播放行为的库。它接受一个apng的buffer数据,并从中提取出每一帧的数据,再逐帧拼装成png格式数据以绘制在canvas上。同时也暴露了一些方法来控制动画的播放次数、暂停等行为。具体使用不在本文阐述,有兴趣可戳连接试用。浏览器
我详细学习了下apng-canvas的解码思路,又看了下PNG和APNG的规范文档,大概有了个概念。(A)PNG文件数据流实际上是一个个数据块(chunks)和文件签名构成。这类文件的签名用8位字节数组表示是(占了8个字节)
export const PNG_SIGNATURE_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // 对应十进制是: export const _PNG_SIGNATURE_BYTES = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
相比于PNG,APNG多了下面这些类型块
块类型 | 必须 | 含义 | 位置与要求 |
---|---|---|---|
acTL | 是 | 动画控制块 | 紧随IHDR块以后 |
fcTL | 是 | 帧控制块 | 1. 第一个fcTL紧随acTL后 2. 以后全部的fcTL都位于每一帧的开头 |
fdAT | 是 | 帧数据块 | 紧随fcTL以后,且至少有一个 |
构成一个apng的核心块以下图(引用源:https://segmentfault.com/a/11...
这些块在apng文件流中的顺序以下:
当时尝试合成apng时,踩坑了很长时间的几个点:
必需要注意图片的尺寸是否设置正确,图片尺寸设置不正确时解析出来的序列帧有问题,同时apng会自动降级为第一个IDAT表示的静态图,以下:(第一个是apng在浏览器中的实际效果,后面三个是解析该apng获得的png的渲染效果)
Apng-canvas 提供了解析、并在canvas中播放apng的能力,咱们能够循着做者的思路反向生成一个apng。核心代码以下,完整代码请戳:apng-handler
interface Params { /* png buffers */ buffers: ArrayBuffer[]; /* 播放次数:0表示无限循环 */ playNum?: number; /* 咱们在此先假设全部帧的尺寸都相同 */ width: number; height: number; } /** * assemble png buffers to apng buffer * 根据png序列生产apng数据 */ export function apngAssembler(params: Params) { const { buffers = [], playNum = 0, width, height } = params; const bb: BlobPart[] = []; /* 1.头8个字节放入PNG签名 */ bb.push(PNG_SIGNATURE_BYTES); // 使用第一帧的 IHDR, IEND, IDAT数据块. 注意 IDAT块可能有多个 let IDATParts: Uint8Array[] = []; let IHDR: Uint8Array; let IEND: Uint8Array; parseChunks(new Uint8Array(buffers[0]), ({ type, bytes, off, length }) => { if (type === "IHDR") { /* 8: 4字节的长度信息 + 4字节的type字符串信息 */ IHDR = bytes.subarray(off + 8, off + 8 + length); } if (type === "IDAT") { IDATParts.push(bytes.subarray(off + 8, off + 8 + length)); } if (type === "IEND") { IEND = bytes.subarray(off + 8, off + 8 + length); } return true; }); /* 2. PNG签名后放入头部信息IHDR块 */ bb.push(makeChunkBytes("IHDR", IHDR)); /* 3. 头部信息以后放入acTL块 */ bb.push(createAcTL(buffers.length, playNum)); /* 4. 放入第一个fcTL控制块 第一个seq是0 */ bb.push(createFcTL({ seq: 0, width, height })); /* 5. 放入 IDAT 块 */ for (let IDAT of IDATParts) { bb.push(makeChunkBytes("IDAT", IDAT)); } /* 6. 从第二帧开始循环存入帧数据fcTL和fdAT */ // 注意如今seq已是1了 let seq = 1; for (let i = 1; i < buffers.length; i++) { /* 6.1 放入fcTL */ bb.push(createFcTL({ seq, width, height })); // 注意fcTL和fdAT共享seq seq += 1; // 拿到当前帧buffer的IDAT块列表 let iDatParts: Uint8Array[] = []; parseChunks(new Uint8Array(buffers[i]), ({ type, bytes, off, length }) => { if (type === "IDAT") { iDatParts.push(bytes.subarray(off + 8, off + 8 + length)); } return true; }); /* 6.2 使用这个IDAT块,生成fdAT */ for (let j = 0; j < iDatParts.length; j++) { bb.push(createFdAT(seq, iDatParts[j])); seq++; } } /* 7. 放入最后一部分IEND块 */ bb.push(makeChunkBytes("IEND", IEND)); // 返回一个Blob对象 return new Blob(bb, { type: "image/apng" }); }
这里最关键的就是fcTL
和acTL
,它们在控制着整个apng的播放行为,好比fcTL用到的控制帧渲染的两个参数:
/** * @see https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk * 渲染下一帧前如何处理当前帧 */ export enum DisposeOP { /* 在渲染下一帧以前不会对此帧进行任何处理;输出缓冲区的内容保持不变。 */ NONE, /* 在渲染下一帧以前,将输出缓冲区的帧区域清除为彻底透明的黑色。 */ TRANSPARENT, /* 在渲染下一帧以前,将输出缓冲区的帧区域恢复为先前的内容。 */ PREVIOUS, } /** * @see https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk * 当前帧渲染时的混合模式 */ export enum BlendOP { /* 该帧的全部颜色份量(包括alpha)都将覆盖该帧的输出缓冲区的当前内容 */ SOURCE, /* 直接覆盖 */ OVER, }
Apng-canvas是一个很棒的库,可是平时都在写业务逻辑代码,不多涉及到字节数组、位运算相关的内容,再加上这个库做者几乎没有什么注释,因此理解这个库里的一些方法仍是要花些时间的。
举个例子:8位字节数组转十进制的位运算版本以下
export const bytes2Decimal = function (bytes: Uint8Array, off: number, bLen = 4) { let x = 0; // Force the most-significant byte to unsigned. x += (bytes[0 + off] << 24) >>> 0; for (let i = 1; i < bLen; i++) x += bytes[i + off] << ((3 - i) * 8); return x; };
写成咱们经常使用的更易理解的方法:
export const _bytes2Decimal = (bytes: Uint8Array, off: number, bLen = 4) => { let x = ""; for (let i = off; i < off + bLen; i++) { // 每一位都转换为2进制并补至8位 x += ("00000000" + bytes[i].toString(2)).slice(-8); } // 再把字符串转为10进制数字返回 return parseInt(x, 2); };
我把这个库外加png合成apng的核心方法放在了一个新的仓库里。使用ts重写了一下,改了一些方法名称、也改变了部分代码结构,更方便阅读理解。仓库地址:apng-handler。但愿能收获一些浏览器环境下压缩apng的pr。
附一张使用代码合成apng的效果图(delay0.1s,dispose采用TRANSPARENT(1)模式:下一帧渲染前清除画布):
最重要的资料,详细解释了每一个apng相比于png增长的一些规范。
W3C的文档,想要深刻了解必须阅读学习的。可是过于专业,我也没有都看完,主要仍是看一些概念性的东西。我想若是之后须要去了解压缩的实现的话必定还要再看看的。
主要就是那张解释图,不少文章都会引用的,我加在README里了
国内网易云前端团队对于apng-canvas的解释,里面的一张图很是不错
生成apng的在线工具
生成、解析apng的一款软件
Join up PNG images to an APNG animated image
回答了一个Node环境下的encode方法
我试用了一次可是失败了,多是用法有问题,另外这个代码也不是很好懂,没有细看了。