From 2432093e0b335abe4fa96875fdcf96f01ae1e761 Mon Sep 17 00:00:00 2001 From: zk-phi Date: Mon, 24 Jun 2024 18:41:02 +0900 Subject: [PATCH] Use global rgb565 palette --- src/gifworker.ts | 62 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/src/gifworker.ts b/src/gifworker.ts index b1a0ace8..cb7c3e17 100644 --- a/src/gifworker.ts +++ b/src/gifworker.ts @@ -1,8 +1,7 @@ -import { GIFEncoder, quantize, applyPalette } from "gifenc"; +import { GIFEncoder, quantize, applyPalette, nearestColorIndex } from "gifenc"; import { binarizeTransparency } from "./utils/binarize"; type Options = { - format: "rgb565" | "rgba4444", delay: number, width: number, height: number, @@ -16,23 +15,64 @@ const encoder = GIFEncoder(); const frames: Uint8ClampedArray[] = []; let options: Options; +// Make an rgba565 global palette. +const quantizeGlobal = ( + frames: Uint8ClampedArray[], + width: number, + height: number, +) => { + const mergedFrames = new Uint8Array(frames.length * width * height * 4); + frames.forEach((frame, ix) => { + mergedFrames.set(frame, ix * width * height); + }); + const palette = quantize(mergedFrames, 256, { format: "rgb565" }); + return palette; +}; + +const genColorFinder = (palette: number[][]) => { + const cache: Record = {}; + return (rgb: [number, number, number]) => { + const key = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + if (cache[key] === undefined) { + cache[key] = nearestColorIndex(palette, rgb); + } + return cache[key]; + }; +}; + ctx.addEventListener("message", (msg) => { if (msg.data.initialize) { - options = { - ...msg.data.initialize, - format: msg.data.initialize.transparent ? "rgba4444" : "rgb565", - }; + options = msg.data.initialize; } else if (msg.data.addFrame) { frames.push(msg.data.addFrame); } else if (msg.data.finish) { - const { format, delay, transparent, width, height } = options; - frames.forEach((frame) => { + const { delay, transparent, width, height } = options; + const palette = quantizeGlobal(frames, width, height); + if (transparent && palette.length === 256) { // preserve 1 room for the transparent color + palette.pop(); + } + const colorFinder = genColorFinder(palette); + const transparentIndex = palette.length; + frames.forEach((frame, frameIx) => { + // dither and binarize transparency first if (transparent) { binarizeTransparency(frame, width, height); } - const palette = quantize(frame, 256, { format, oneBitAlpha: transparent }); - const index = applyPalette(frame, palette, format); - encoder.writeFrame(index, width, height, { palette, delay, transparent }); + // apply palette + const indexed = new Uint8Array(width * height); + for (let i = 0; i < indexed.length; i++) { + if (transparent && frame[i * 4 + 3] < 128) { + indexed[i] = transparentIndex; + } else { + indexed[i] = colorFinder([frame[i * 4], frame[i * 4 + 1], frame[i * 4 + 2]]); + } + } + encoder.writeFrame(indexed, width, height, { + palette: frameIx === 0 ? palette : undefined, + delay, + transparent, + transparentIndex, + }); }); encoder.finish(); ctx.postMessage(new Blob([encoder.bytes()], { type: "image/gif" }));