利用 JavaScript 将多张 GIF 图合并为一张
基本思路
解析 DOM 中包含的 GIF
重新生成 GIF 的每一帧
解析 GIF 插件 gifuct-js
插件解析出来每一帧的数据结构如下:
{
// The color table lookup index for each pixel
pixels: [...], // the dimensions of the gif frame (see disposal method)
dims: {
top: 0,
left: 10,
width: 100,
height: 50
}, // the time in milliseconds that this frame should be shown
delay: 50, // the disposal method (see below)
disposalType: 1, // an array of colors that the pixel data points to
colorTable: [...], // An optional color index that represents transparency (see below)
transparentIndex: 33, // Uint8ClampedArray color converted patch information for drawing
patch: [...]
}
patch 表示当前帧图,数据格式是 Uint8ClampedArray,可以使用 putImageData 在 canvas 渲染。
ctx.putImageData(newImageData(patch, dims.width, dims.height),dims.left,dims.top);
生成 GIF 插件 gif.js
const gif = newGIF({ workers: 2, quality: 10 });
gif.addFrame(imageElement, { delay: 50 });
gif.on('finished', (blob) => {
window.open(URL.createObjectURL(blob));
});
gif.render();
gif.js 插件没有 npm 包,所以只能将相关文件拷贝到项目中,通过 script 标签引入来使用。
FAQ
1.多个 GIF 素材时长不一致,如何保证同时结束?
a.场景一
- GIFa 时长 900ms
- GIFb 时长 1300ms
假设生成的 GIF 时长 1200ms,GIFb 正常结束,GIFa 丢失 100ms。
解决方案:取平均值
平均值:(900 + 1300) / 2 = 1100 ms
- GIFa 增加 200ms,假设总共 20 帧,单帧时长 50ms。则每帧增加 200 / 20 = 19ms,为 60ms。
- GIFb 减少 200ms,假设总共 24 帧,单帧时长 50ms。则每帧减少 200 / 24 = 8.33ms,为 41.67ms。
b.场景二
- GIFa 时长 1200ms
- GIFb 时长 3200ms
生成的 GIF 时长 3200ms,GIFb 正常结束,GIFa 轮播 2 次,第 3 次播放 800ms,丢失 400ms。
解决方案:
1.计算最大公倍数
时长会很大,导致数据量很大。❌
2.计算最小时长与最大时长的合理倍数关系,最小时长 * 倍数。
function computeTotalTime(min: number, max: number) {
const div = max / min;
const decimal = div % 1;
const multiple = decimal > 0.5 ? Math.ceil(div) : Math.floor(div);
return min * multiple;
}
合理倍数关系,GIFa 与 GIFb 的合理倍数应该是 2 还是 3?
如果是 2,则二者差距是 3200 - (1200 * 2) = 800ms;
如果是 3,则二者差距是 (1200 * 3) - 3200 = 400ms;
差距越小越好,所以合理倍数关系为 3,总时长为 3600ms。
调整每张 GIF 图每帧的时长,适配总时长。
// 调整多张 gif 图的数据,适配总时长
function adaptGifFrames(gifsData: GifData[], time: number) { // 适配时长
gifsData.forEach((gifData) => { const { totalTime, frames } = gifData; // 目标时长与当前 gif 图时长的倍数关系
const multiple = Math.floor(time / totalTime); // 目标时长与当前 gif 图时长的差值
let diff = time - totalTime; if (multiple !== 0) {
diff = (time - multiple * totalTime) / multiple;
} if (diff === 0) return;
frames.forEach((frame) => { // 依据每一帧占比增加时长
const precent = frame.delay / totalTime;
frame.delay += diff * precent;
}); // 设置适配后的总时长
gifData.totalTime = Math.round(
frames.reduce((pre, cur) => pre + cur.delay, 0)
);
});
}
2.生成 GIF 图时,总共多少帧?每帧的延时时长是多少?
理想情况下,每帧延时时长为 50ms,但所使用的 GIF 图来源不一,无法保证其一致性。另外总时长越长,帧数就越多,体积就越大,GIF 图生成时间就越长。
所以限制帧数最大值为 100,如果超过 100,则每帧增加相应时长。
function computeGifData(gifsData: GifData[]) { const times = gifsData.map((gif) => gif.totalTime); const minTime = Math.min(...times); const maxTime = Math.max(...times); const div = maxTime / minTime; let totalTime = 0; if (div > 2) { // 计算最大时长
totalTime = computeTotalTime(minTime, maxTime);
} else { // 取平均时长
totalTime = times.reduce((pre, cur) => pre + cur, 0) / times.length;
} let delay = DEFAULT_GIF_DELAY; let frameLen = totalTime / delay; // 帧数
if (frameLen > MAX_GIF_FRAME) {
delay = delay * (frameLen / MAX_GIF_FRAME);
frameLen = MAX_GIF_FRAME;
} return { totalTime, frameLen, delay };
}
根据帧数 frameLen 循环获取每一帧的图片,延时时长为 delay。
gif.addFrame(image, { delay });
3.字体较多导致生成速度较慢
中文字体文件体积大多以 M 为单位,导致每帧图片的 svg 体积较大,继而导致每帧图片加载耗时较长。
上图 3000ms ~ 10000ms,近 7s 时间都是在加载每帧的图片。
百度前端团队有个 NodeJS 工具 fontmin,可以按照指定文字内容对字体文件进行裁剪,返回仅包含指定文字内容的字体文件,可以极大的减少字体体积。
使用这个工具后,GIF 图生成速度得到显著提升。