apng逐漸成為大部分業務實現複雜動效、動畫的方案。這種方案有下面幾個優點:
我們的智慧輔播業務也有這樣的使用場景。如下圖
圖片可能會被降級點選檢視:https://gw.alicdn.com/imgextr...
上面這張圖在設計師通過軟體制作出來時,是一個無限迴圈的apng檔案。所以不加處理直接展示在裝置上時將會迴圈播放。而下面這幅圖在設計出來就是一個播放1次的動畫(如果沒看到動作可以直接複製圖片連結在瀏覽器開啟。
圖片可能會被降級,點選檢視:https://gw.alicdn.com/imgextr...
一個良好的網頁應該遵循基本的規範,比如W3C無障礙規範中明確的:
不要設計會導致癲癇發作或身體反應的內容。
網頁不包含任何閃光超過3次/秒的內容,或閃光低於一般閃光和紅色閃光閾值。.
所以頁面上的動畫不應該一直重複播放(一方面會奪了使用者的焦點,另一方面令人煩躁)。在智慧輔播的業務中,我們規定了動畫只在獲取到小助理新的對話內容的時候才播放一次。
在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
* 渲染下一幀前如何處理當前幀
*/
export enum DisposeOP {
/* 在渲染下一幀之前不會對此幀進行任何處理;輸出緩衝區的內容保持不變。 */
NONE,
/* 在渲染下一幀之前,將輸出緩衝區的幀區域清除為完全透明的黑色。 */
TRANSPARENT,
/* 在渲染下一幀之前,將輸出緩衝區的幀區域恢復為先前的內容。 */
PREVIOUS,
}
/**
* @see https://wiki.mozilla.org/APNG_Specification
* 當前幀渲染時的混合模式
*/
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方法
我試用了一次但是失敗了,可能是用法有問題,另外這個程式碼也不是很好懂,沒有細看了。