动图比想象中慢半拍,多半不是播放器的问题
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。