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方法
我試用了一次可是失敗了,多是用法有問題,另外這個代碼也不是很好懂,沒有細看了。