关于javascript:聊聊APNG

7次阅读

共计 4850 个字符,预计需要花费 13 分钟才能阅读完成。

背景

apng 逐步成为大部分业务实现简单动效、动画的计划。这种计划有上面几个长处:

  1. 相比于 gif,画质更好,尤其对于带透明度的图片。具体比拟请自行 google
  2. 自身其实是一个 png 文件,在不反对 apng 的设施上时,能降级显示一个 png 静图(前面会讲到)
  3. 能够间接作为 img 标签插入到网页中去,无需逻辑管制动画,开发成本低
  4. 间接由设计师产出,设计还原度 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 时,踩坑了很长时间的几个点:

  1. 必须要有 IDAT 块,这个块通常取自第一帧 png 的 IDAT 块,这个块的作用就是在一些不反对 apng 的环境中作为降级的 png 应用
  2. fcTL 和 fdAT 块共享顺序号(sequence),这个号从 0 开始,即第一个 IDAT 前的 fcTL 的 sequence 为 0
  3. IDAT 可能存在多个,须要顺次序放入数据流中
  4. 必须要留神图片的尺寸是否设置正确,图片尺寸设置不正确时解析进去的序列帧有问题,同时 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"});
}

这里最要害的就是 fcTLacTL,它们在管制着整个 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)模式:下一帧渲染前革除画布):

附录

  1. APNG 标准

    最重要的材料,具体解释了每个 apng 相比于 png 减少的一些标准。

  2. W3C PNG 标准

    W3C 的文档,想要深刻理解必须浏览学习的。然而过于业余,我也没有都看完,次要还是看一些概念性的货色。我想如果当前须要去理解压缩的实现的话肯定还要再看看的。

  3. APNG 维基百科

    次要就是那张解释图,很多文章都会援用的,我加在 README 里了

  4. Web 端 APNG 播放实现原理

    国内网易云前端团队对于 apng-canvas 的解释,外面的一张图十分不错

  5. ezgif.com

    生成 apng 的在线工具

  6. APNG Assembler

    生成、解析 apng 的一款软件

  7. Join up PNG images to an APNG animated image

    答复了一个 Node 环境下的 encode 办法

  8. UPNG.js

    我试用了一次然而失败了,可能是用法有问题,另外这个代码也不是很好懂,没有细看了。

正文完
 0