@techsquidtv/gifenc
v1.1.0
Published
very fast JS GIF encoder
Readme
@techsquidtv/gifenc
A fork of Matt DesLauriers' gifenc
with additional features, modernized, and ported to Typescript. AI assisted.
@techsquidtv/gifenc is a fast, lightweight JavaScript GIF encoder for Node.js,
browsers, and workers. It keeps the original low-level approach: you control
palette generation, palette application, and frame writing instead of handing
everything to a black-box encoder.
Tribute
This project exists because of Matt DesLauriers' original gifenc, a tiny and
clever pure-JavaScript GIF encoder. The core API shape, stream encoder, and
performance-minded design come from that work.
This fork keeps that lineage visible while adding newer package tooling and
features under the @techsquidtv/gifenc npm scope.
What's Different
- Published as
@techsquidtv/gifenc. - TypeScript source with generated declaration files.
- Modern package exports for ESM and CommonJS consumers.
- Floyd-Steinberg dithering support in
applyPalette. - Updated examples, benchmarks, development tooling, and release automation.
Install
npm install @techsquidtv/gifencimport {
GIFEncoder,
quantize,
applyPalette,
createTemporalDither,
} from "@techsquidtv/gifenc";For direct browser imports, use the ESM build:
import {
GIFEncoder,
quantize,
applyPalette,
createTemporalDither,
} from "https://unpkg.com/@techsquidtv/gifenc/dist/gifenc.mjs";Quick Start
import { GIFEncoder, quantize, applyPalette } from "@techsquidtv/gifenc";
const { data, width, height } = getImageDataSomehow();
const palette = quantize(data, 256);
const index = applyPalette(data, palette);
const gif = GIFEncoder();
gif.writeFrame(index, width, height, { palette });
gif.finish();
const bytes = gif.bytes();For an animation, call writeFrame for each frame before finish:
const gif = GIFEncoder();
for (const frame of frames) {
const palette = quantize(frame.data, 256);
const index = applyPalette(frame.data, palette);
gif.writeFrame(index, frame.width, frame.height, {
palette,
delay: 100,
});
}
gif.finish();
const bytes = gif.bytes();Dithering
Use Floyd-Steinberg dithering when mapping RGBA pixels to a reduced palette. This can help gradients, photographs, and other continuous-tone images avoid obvious banding.
const format = "rgb565";
const palette = quantize(data, 256, { format });
const index = applyPalette(data, palette, {
format,
dither: "floyd-steinberg",
width,
height,
});You can also tune the dither pass:
const index = applyPalette(data, palette, {
dither: "floyd-steinberg",
width,
height,
ditherStrength: 0.75,
serpentine: true,
});For animations, temporal dithering can carry each pixel's quantization error into the next frame. Create one state object per animation and reuse it while mapping frames:
const temporalDither = createTemporalDither({ width, height, format });
for (const frame of frames) {
const palette = quantize(frame.data, 256, { format });
const index = applyPalette(frame.data, palette, {
format,
dither: "floyd-steinberg",
temporalDither,
});
gif.writeFrame(index, width, height, { palette, delay });
}Temporal dithering state is mutable and sequence-scoped. Do not share one state
between unrelated animations or concurrent encodes; create separate states, or
call reset() before reusing a state for a new sequence.
By default, temporal dithering also uses change detection to reject stale history across large motion or scene changes. This clears carried error for changed pixels, and resets the whole history when most of the frame changes:
const temporalDither = createTemporalDither({
width,
height,
format,
changeDetection: {
pixelThreshold: 48,
sceneChangeRatio: 0.75,
},
});Set changeDetection: false if you want exact residual carry between every
frame or prefer to call reset() manually at known cuts.
API
quantize(rgba, maxColors, options)
Builds a reduced color palette from RGBA pixel data.
rgba:Uint8ArrayorUint8ClampedArraycontaining RGBA pixels.maxColors: maximum palette size, usually256or less.options.format:"rgb565","rgb444", or"rgba4444".options.oneBitAlpha: converts alpha to fully transparent or fully opaque.options.clearAlpha: clears RGB channels for transparent colors.
Returns a palette such as:
[
[0, 255, 10],
[50, 20, 100],
];applyPalette(rgba, palette, options)
Maps RGBA pixels to palette indexes.
rgba: source RGBA pixel data.palette: palette returned byquantizeor supplied by your app.options: either a format string or an options object.options.dither:false,true, or"floyd-steinberg".options.width: required when dithering is enabled.options.height: optional consistency check for dithered input.options.ditherStrength: scales propagated quantization error.options.serpentine: alternates scan direction per row.options.temporalDither: state returned bycreateTemporalDither().
Returns a Uint8Array with one palette index per pixel.
createTemporalDither(options)
Creates resettable temporal dithering state for an animation.
options.width: frame width in pixels.options.height: frame height in pixels.options.format:"rgb565","rgb444", or"rgba4444".options.strength: scales previous-frame carried error.options.decay: scales newly carried error for the next frame.options.maxError: clamps carried per-channel error.options.changeDetection: rejects stale temporal history after large source changes. Defaults totrue, withpixelThreshold: 48andsceneChangeRatio: 0.75.
Call state.reset() before reusing the state for an unrelated animation, or at
known scene boundaries when managing cuts yourself.
GIFEncoder(options)
Creates an encoder stream.
options.auto: whentrue, writes the GIF header and first-frame metadata on the first frame.options.initialCapacity: starting internal buffer size.
Common methods:
writeFrame(index, width, height, options): writes one indexed frame.finish(): writes the GIF trailer.bytes(): returns a copiedUint8Array.bytesView(): returns a direct view into the encoder buffer.writeHeader(): writes a header manually whenautois disabled.reset(): reuses the encoder buffer for another GIF.
Frame options include:
palette: color table for the frame.delay: frame delay in milliseconds.repeat: animation repeat count, where0means forever.transparent: enables one-bit transparency.transparentIndex: palette index to treat as transparent.dispose: GIF disposal method override.
Color Helpers
nearestColorIndex(palette, pixel): returns the closest palette index.nearestColorIndexWithDistance(palette, pixel): returns[index, distance].prequantize(rgba, options): reduces RGBA precision before quantization.
Web Workers
gifenc works well in workers because quantization, palette mapping, and frame
encoding can be split across frames.
A common pattern:
- Send RGBA frame data to workers.
- In each worker, call
quantize,applyPalette, andGIFEncoder. - Return encoded frame chunks to the main thread.
- Combine chunks into one final GIF stream.
See the local worker example:
How GIF Encoding Works
GIF encoding usually has three steps:
- Quantize RGBA pixels into a palette of 256 colors or fewer.
- Map each RGBA pixel to the nearest palette index.
- Write indexed frames and palettes into a GIF stream.
This package exposes all three steps so you can make tradeoffs per project: use one palette for an entire animation, quantize each frame independently, enable dithering for gradients, or skip quantization entirely if your input is already indexed.
Examples
Node examples:
pnpm run build
node examples/node/encode.ts
node examples/node/encode-dither.tsBrowser examples:
pnpm run build
pnpm run serveThen open:
- http://localhost:5000/examples/browser/
- http://localhost:5000/examples/browser/encode-workers.html
- http://localhost:5000/bench/browser/
- http://localhost:5000/bench/video-report/
The video GIF benchmark report can also be published manually to GitHub Pages
from the Benchmark Pages workflow. The hosted report remains browser-based:
visitors click Run benchmark, and their browser decodes the MP4 and encodes
the GIF variants. Once deployed, open:
Development
Use Node.js 24.16.0 LTS with pnpm 11 for local development. The tooling supports Node.js 22.13.0 and newer.
pnpm install
pnpm run build
pnpm run checkUseful scripts:
pnpm run build: builds ESM, CommonJS, and types intodist.pnpm run build:benchmark-pages: builds the static GitHub Pages benchmark artifact into_site.pnpm run check: runs formatting, linting, type checks, and dependency checks.pnpm run serve: starts the local example server.
Publishing
This fork publishes publicly to npm as @techsquidtv/gifenc. Releases are
intended to be created through the GitHub release workflow so version bumps,
tags, generated release notes, and npm publishing stay together.
The release workflow determines the version bump from commits since the latest
v* tag. Breaking-change markers create a major release, feat commits create
a minor release, and fix, perf, or security commits create a patch
release.
Credits
Created from Matt DesLauriers' gifenc.
Thank you to Matt for the original encoder and API design.
This project also builds on ideas and prior art from:
License
MIT. See LICENSE.md.
