雜記 › 生活隨筆 › 幀間隔填 137 ms,成品每幀其實停 140 ms:tick 量化、4096 頁填充與跟著第一張走的畫布

幀間隔填 137 ms,成品每幀其實停 140 ms:tick 量化、4096 頁填充與跟著第一張走的畫布

幀間隔填 137 ms,成品每幀實停 140 ms:tick 量化台階、49 和 abc 都鉗到 50 的鉗位真值表、4096 頁填零與跟著第一張走的畫布,全部從產物位元組直讀。

動態圖比想像中慢半拍,多半不是播放器的問題

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 也照樣進編碼器。

圖 1 填的毫秒數落進 tick 台階 50 135 250 333 105 → 110 137 → 140 333 → 330 橫軸 填的 ms

鉗位真值表:49 和 abc 都落到 50,1e3 卻活了

頁面文案把幀間隔寫成 100ms ~ 3000ms,可第 21 行的 min=50、第 280 行 if (v < 50) 的下鉗位也是 50,50 到 99 這一檔沒提卻實測可達。髒寫法餵進同一個輸入框的結果:

寫入的值生效延時成品 tick每幀停留
49 / 0 / -550 ms550 ms
12.5 / 空字串 / abc50 ms550 ms
3001 / 1000003000 ms3003000 ms
1e31 ms00,不回落 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 單幀,不是普遍比例。

圖 2 填充占下載體積的比例 80.0% 78.6% 63.3% 1.8% 0.06% 1×1 單幀 32×32 單幀 純色單幀 漸層 10 幀 雜訊 10 幀

另兩筆帳跟幀數有關。標頭恆 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。

📎 相關文章

同一串 23 位元組的內容切換 4 個糾錯等級:這一頁自己寫的二維碼編碼器走到了哪一步 2026-10-30 拼豆圖紙的色號是怎麼挑出來的:開一次抖動,1024 格裡有 150 格被改寫 2026-10-30 橫圖裁掉 25%、豎圖裁掉 43.75%:7 個拼圖模板逐格算一筆幾何帳 2026-10-28 淺底白字浮水印設到 5% 等於沒加:匯出 4782 位元組與底圖一位元組不差 2026-10-28 28 天還是 29 天:1 月 31 日加一個月要自己換算 2026-10-28