gif2asciiart
v1.0.0
Published
Zero-dependency GIF89a parser + GIF to ASCII animation library for browser and Node.js
Maintainers
Readme
gif2asciiart
GIF 转 ASCIIART 动画库
特性
- 完整帧合成 — 正确处理 none / background / previous 三种 disposal 模式
- LZW 解压 — 变长编码 + clear/EOI + 链表字典(O(k) 展开)
- ASCII 映射 — 可配置字符池 + 灰度 LUT 预计算 + 亮度阈值过滤
- 双格式产物 — esbuild 打包 ESM + IIFE(浏览器全局),含 sourcemap
安装
npm install gif2asciiart或直接使用构建产物(不经过 npm):
git clone <repo>
cd gif2asciiart
npm install
npm run build # 产出 dist/用法
Node.js / ESM
import { gifToAscii } from 'gif2asciiart';
import { readFileSync } from 'fs';
const buf = readFileSync('animation.gif');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const data = gifToAscii(ab, {
sampleX: 4,
sampleY: 4,
brightnessCutoff: 50,
asciiChars: { dark: ['0', '1'], gray: ['.', '-'], light: [' '] },
});
for (const frame of data.frames) {
console.log(frame.c); // ASCII 字符串,长度 w*h
console.log(`延迟 ${frame.d}ms`);
}浏览器(IIFE 全局)
<script src="gif2asciiart.browser.js"></script>
<script>
// 通过 input[type=file] / 拖拽 / 粘贴获取 File 对象
const reader = new FileReader();
reader.onload = () => {
const data = gif2asciiart.gifToAscii(reader.result, { sampleX: 4, sampleY: 4 });
console.log(data.w, data.h, data.total);
};
reader.readAsArrayBuffer(file);
</script>注意:
file://协议下fetch('x.gif')会失败,务必使用FileReader读取本地文件。
浏览器(ESM)
<script type="module">
import { gifToAscii } from './gif2asciiart.esm.js';
</script>构建
npm run build产出(dist/):
| 文件 | 格式 | 全局变量 |
|------|------|----------|
| gif2asciiart.esm.js | ESM | — |
| gif2asciiart.browser.js | IIFE(压缩) | gif2asciiart |
| *.js.map | sourcemap | — |
API
gifToAscii(arrayBuffer, opts?)
核心函数。Node.js 与浏览器通用。
| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| arrayBuffer | ArrayBuffer | — | 原始 GIF 二进制数据 |
| opts.sampleX | number | 2 | 水平采样间隔 (1-1000) |
| opts.sampleY | number | 2 | 垂直采样间隔 (1-1000) |
| opts.brightnessCutoff | number | 0 | 亮度阈值 (0-255),灰度低于此值渲染为空格 |
| opts.asciiChars | {dark, gray, light} | 见下 | 按亮度分类的字符数组 |
| opts.grayscaleMap | Array<{max, pool}> | 见下 | 灰度区间 → 字符池映射 |
| opts.invertChars | boolean | false | 反转字符集,暗部用 light 池,亮部用 dark 池 |
默认字符池:
{ dark: ['0', '1'], gray: ['.', '-'], light: [' '] }默认灰度映射:
[
{ max: 85, pool: 'dark' }, // 0-85 → dark 池
{ max: 170, pool: 'gray' }, // 86-170 → gray 池
{ max: 255, pool: 'light' }, // 171-255 → light 池
]返回值:
{
w: number; // 采样后宽度(字符列数)
h: number; // 采样后高度(字符行数)
ow: number; // 原始像素宽度
oh: number; // 原始像素高度
total: number; // 总帧数
loop: number; // 循环次数 (0=无限)
frames: Array<{
c: string; // 扁平 ASCII 字符串,长度 w*h,按 c[y*w + x] 索引
d: number; // 帧延迟(毫秒)
}>;
}过滤参数
brightnessCutoff — 灰度值(0.299R + 0.587G + 0.114B)低于此阈值的像素渲染为空格,用于提亮画面、去除噪点。
技术细节
Disposal Method(处理方式)
GIF 动画的每一帧可能不是完整的图像,而是一个需要绘制到特定位置的"补丁"。disposalType 决定了在绘制下一帧之前如何处理当前画布:
| 值 | 含义 | 处理方式 | |----|------|----------| | 0 | 未指定 | 不做任何处理 | | 1 | 保留 | 下一帧直接覆盖绘制到当前画布上(最常见) | | 2 | 恢复背景 | 绘制完当前帧后,将画布恢复为背景色 | | 3 | 恢复前一帧 | 绘制完当前帧后,将画布恢复为前一帧的状态 |
本库正确处理所有 disposal 类型,确保动画显示正确。
Transparency(透明度)
GIF 支持通过透明索引指定某种颜色为透明。当帧的 transparentIndex 被定义时,像素数据中匹配该索引的像素不会被绘制。在转换为 ASCII 时,透明像素会被保留为空白或背景字符。
Interlacing(隔行扫描)
GIF 支持隔行扫描存储,像素数据按 4-pass 方式排列:
Pass 1: 行 0, 8, 16, 24... (起点 0,步长 8)
Pass 2: 行 4, 12, 20, 28... (起点 4,步长 8)
Pass 3: 行 2, 6, 10, 14... (起点 2,步长 4)
Pass 4: 行 1, 3, 5, 7... (起点 1,步长 2)本库自动检测并处理隔行扫描的 GIF,输出时已还原为正常行顺序。
LZW 解压
GIF 使用 LZW (Lempel-Ziv-Welch) 压缩算法。本库实现了完整的 LZW 解压器:
- 变长编码(从 minCodeSize+1 开始,最大 12 位)
- Clear Code 和 End-of-Information Code 处理
- 链表字典实现,O(k) 时间展开每个码字
字符映射原理
像素 RGB → gray = 0.299R + 0.587G + 0.114B → [0, 255]
↓ LUT 查表 (256项预计算)
暗 ───────── 灰 ─────────── 亮
0 85 171 255
[0, 1] [., -] [ ]buildMapper 在调用时一次性构建 256 项查找表,后续每个采样点 O(1) 映射。
项目结构
gif2asciiart/
├── src/
│ └── converter.js 核心:GIF解析 + LZW + 去隔行 + 帧合成 + ASCII 映射
├── dist/ 构建产物(ESM + IIFE + sourcemap)
├── build.js esbuild 构建脚本
├── package.json
└── LICENSE