動態圖比想像中慢半拍,多半不是播放器的問題
6 張介面截圖做成一段動態圖,幀間隔填的是 137 ms,下載後丟進時間軸一量,每幀停的是 140 ms。差的 3 ms 沒丟,是被格式收走的:GIF 的延時欄位只有百分之一秒這一檔格子。同一頁還有兩處跟著別的東西走——畫布跟著第一張走,下載體積裡有一段純填零。這三件事湊在一起,就是慢半拍、順序一動全變、小圖虛高。
137 ms 是怎麼變成 14 tick 的
在多圖合成 GIF這一頁,延時要過好幾道手:輸入框第 21 行的屬性是 min=50、max=3000、step=50;第 278–282 行先鉗位;第 184 行走 parseInt(value) || 300;第 228 行把毫秒整數交給 gif.js;worker 裡做 Math.round(137 / 10) 得到 tick,寫成小端 2 位元組的延時。
22 個值逐一實際跑過,tick 直接從產物 GIF 的圖形控制擴充第 4–5 位元組讀出:137 → tick 14 → 實際 140 ms(+3);333 → 33 → 330 ms(-3)。誤差絕對值最大 5 ms,ms 是 10 的倍數時恆為 0。step=50 只是屬性、程式碼從不查 validity,137 讓元素 :invalid 也照樣進編碼器。
鉗位真值表:49 和 abc 都落到 50,1e3 卻活了
頁面文案把幀間隔寫成 100ms ~ 3000ms,可第 21 行的 min=50、第 280 行 if (v < 50) 的下鉗位也是 50,50 到 99 這一檔沒提卻實測可達。髒寫法餵進同一個輸入框的結果:
| 寫入的值 | 生效延時 | 成品 tick | 每幀停留 |
|---|---|---|---|
49 / 0 / -5 | 50 ms | 5 | 50 ms |
12.5 / 空字串 / abc | 50 ms | 5 | 50 ms |
3001 / 100000 | 3000 ms | 300 | 3000 ms |
1e3 | 1 ms | 0 | 0,不回落 300 |
空字串這條值得停一下:"" < 50 數值化成 0、比較成立,於是直接鉗成 50,根本輪不到 || 300 兜底。1e3 是唯一逃過鉗位的寫法:兩個比較都拿 NaN、都不成立,值原樣留著,最後 parseInt("1e3") 讀出 1 → tick 0,產物裡的延時真是 0。
預覽回顯的幀速和成品的延時不是一個數
第 167 行按使用者 ms 算 (1000 / delay).toFixed(1) 回顯:333 → 3.0;成品用的是 tick×10 ms,兩邊只在 ms 是 10 的倍數時相等。而預覽播的不是編碼後的 GIF:第 153–171 行放源圖陣列,每幀依第 159 行各自縮放並重寫 canvas 尺寸,成品卻在第 183 行統一成第一張的尺寸加黑邊置中。
畫布跟著第一張走,順序一動全變
第 183 行取的是 imgList[0].w 和 imgList[0].h——第一張,不取最大。首圖 400×400 時,混進來的一張 200×120 以 [0,80,400,240] 當參數被放大 2.0 倍鋪上去;首圖換成 200×120,那張 400×400 就被壓成 [40,0,120,120]、左右各留 40 px 黑邊。
縮圖本來就能拖著換順序,代價是整份檔案跟著變:把原本排在末位的 200×120 拖到第 1 位,畫布從 400×400 變成 200×120,blob 從 26267 變成 14660。要控畫布就把基準圖放佇列最前面。拆幀是另一條路,口徑歸按幀匯出那篇;選圖與排版是拼圖那條線的事。
另外,第 217–219 行每幀先清螢幕再鋪 #000000 底,帶 alpha 的源被寫成 [0,0,0,255];gif.js 的 transparent 選項本模板沒使用,成品 GIF 沒有透明索引、只有黑底。
下載體積:4096 一頁的填零與跟著幀數的調色盤
兩個長度要分清:blob.size 是下載到的,realBytes 是走到結束符 0x3B 的,差值全是 0x00 填充,來自 gif.js 裡 ByteArray 的 pageSize=4096。實測 1×1 單幀 real 826 → blob 4096,填充 3270、佔 80.0%;幀多或像素多就攤薄到 2% 以下(漸層 10 幀 real 206833 → blob 210728,只剩 1.8%)。80.0% 只屬於 1×1 單幀,不是普遍比例。
另兩筆帳跟幀數有關。標頭恆 781 B(GIF89a 的 6 加邏輯螢幕描述元 7 加全域調色盤 768),跟幀數無關。第 1 幀重用全域板,第 2 幀起每幀自帶 768 B 區域板,localPaletteTotal 完全等於 (N-1)×768,40 幀就是 29952 B。同畫布同幀數只換內容,每幀 LZW 就是純色 678 B、照片 24829 B、雜訊 218304 B,差 322 倍。
大畫布的口徑必須帶反證:那張 6000×4000 的 JPEG 單幀 LZW 18791 B,可這次編碼拿到的緩衝在 4000 個取樣點上 distinct=1、全是 [0,0,0,255]——本機把這張 24 Mpx 的圖解碼成了均勻黑,所以 18791 B 只是單色幀的下限(0.00078 B/px),不能當大照片的體積;照片口徑要用 photo_400.jpg 的 0.155 B/幀·px。
編碼器這一環要連線
冷啟動時 window.GIF 是 undefined,第 262 行注入 cdnjs 的 gif.js 0.2.0,實測 decodedBodySize 13451 B;onerror 換第 269 行的 unpkg 來源,兩者都失敗才 reject「CDN 全部不可用」。gif.worker.js 實測 16636 B,由第 196 行 fetch 回來再建立 Worker。像素本身確實不出機器:第 84 行 img.src 用 dataURL,全模板只有第 196 行這一處 fetch。
常被問到的三條
Q:幀間隔只能填 50 的倍數嗎?
不是。step=50 只是屬性、程式碼不查 validity,137 和 333 都照送,代價是被 Math.round(ms / 10) 收進台階、最大偏差 5 ms。想嚴格一致就填 10 的倍數並落在 50 到 3000 之間,1e3 這類寫法更會被讀成 1。
Q:幾張極小的圖為什麼比源圖加起來還大?
兩筆額外開銷:gif.js 的 4096 頁填零,和第 2 幀起每幀 768 B 的區域調色盤——鋪到 40 幀時 (N-1)×768 等於 29952 B 全是調色盤。
Q:匯出前怎麼確認畫布尺寸?
看第一位那張,第 183 行只認 imgList[0]。把 200×120 拖到第 1 位,畫布從 400×400 變成 200×120、blob 從 26267 變成 14660。