背景
apng 逐步成为大部分业务实现简单动效、动画的计划。这种计划有上面几个长处:
- 相比于 gif,画质更好,尤其对于带透明度的图片。具体比拟请自行 google
- 自身其实是一个 png 文件,在不反对 apng 的设施上时,能降级显示一个 png 静图(前面会讲到)
- 能够间接作为 img 标签插入到网页中去,无需逻辑管制动画,开发成本低
- 间接由设计师产出,设计还原度 100%
咱们的智能辅播业务也有这样的应用场景。如下图
图片可能会被降级点击查看:https://gw.alicdn.com/imgextr…
下面这张图在设计师通过软件制作进去时,是一个有限循环的 apng 文件。所以不加解决间接展现在设施上时将会循环播放。而上面这幅图在设计进去就是一个播放 1 次的动画(如果没看到动作能够间接复制图片链接在浏览器关上。
图片可能会被降级,点击查看:https://gw.alicdn.com/imgextr…
一个良好的网页应该遵循根本的标准,比方 W3C 无障碍标准中明确的:
不要设计会导致癫痫发生或身材反馈的内容。
网页不蕴含任何闪光超过 3 次 / 秒的内容,或闪光低于个别闪光和红色闪光阈值。.
除非动画对于性能或传播的信息至关重要,否则能够禁用由交互触发的交互式动画。
所以页面上的动画不应该始终反复播放(一方面会夺了用户的焦点,另一方面令人焦躁)。在智能辅播的业务中,咱们规定了动画只在获取到小助理新的对话内容的时候才播放一次。
在 weex 环境下,咱们的设计师间接产出一个不循环播放的 apng 文件,前端只须要加载即可。在 h5 环境下,其实咱们能间接管制 apng 的播放。
apng-canvas
apng-canvas 是一个用于在浏览器环境下管制 apng 文件播放行为的库。它承受一个 apng 的 buffer 数据,并从中提取出每一帧的数据,再逐帧拼装成 png 格局数据以绘制在 canvas 上。同时也裸露了一些办法来管制动画的播放次数、暂停等行为。具体应用不在本文论述,有趣味可戳链接试用。
(A)PNG 标准
我具体学习了下 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]);
apng 的标准
- apng 标准文档戳此处
- PNG 标准戳此处
相比于 PNG,APNG 多了上面这些类型块
块类型 | 必须 | 含意 | 地位与要求 |
---|---|---|---|
acTL | 是 | 动画管制块 | 紧随 IHDR 块之后 |
fcTL | 是 | 帧管制块 | 1. 第一个 fcTL 紧随 acTL 后 2. 之后所有的 fcTL 都位于每一帧的结尾 |
fdAT | 是 | 帧数据块 | 紧随 fcTL 之后,且至多有一个 |
形成一个 apng 的外围块如下图(援用源:https://segmentfault.com/a/11…
这些块在 apng 文件流中的程序如下:
过后尝试合成 apng 时,踩坑了很长时间的几个点:
- 必须要有 IDAT 块,这个块通常取自第一帧 png 的 IDAT 块,这个块的作用就是在一些不反对 apng 的环境中作为降级的 png 应用
- fcTL 和 fdAT 块共享顺序号(sequence),这个号从 0 开始,即第一个 IDAT 前的 fcTL 的 sequence 为 0
- IDAT 可能存在多个,须要顺次序放入数据流中
-
必须要留神图片的尺寸是否设置正确,图片尺寸设置不正确时解析进去的序列帧有问题,同时 apng 会主动降级为第一个 IDAT 示意的动态图,如下:(第一个是 apng 在浏览器中的实际效果,前面三个是解析该 apng 失去的 png 的渲染成果)
由 png 合成 apng
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 标准
最重要的材料,具体解释了每个 apng 相比于 png 减少的一些标准。
-
W3C PNG 标准
W3C 的文档,想要深刻理解必须浏览学习的。然而过于业余,我也没有都看完,次要还是看一些概念性的货色。我想如果当前须要去理解压缩的实现的话肯定还要再看看的。
-
APNG 维基百科
次要就是那张解释图,很多文章都会援用的,我加在 README 里了
-
Web 端 APNG 播放实现原理
国内网易云前端团队对于 apng-canvas 的解释,外面的一张图十分不错
-
ezgif.com
生成 apng 的在线工具
-
APNG Assembler
生成、解析 apng 的一款软件
-
Join up PNG images to an APNG animated image
答复了一个 Node 环境下的 encode 办法
-
UPNG.js
我试用了一次然而失败了,可能是用法有问题,另外这个代码也不是很好懂,没有细看了。